feat(bridge): Discord badge on bridged users and a header chip on bridged rooms #479
@@ -0,0 +1,109 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import { Chip, Icon, IconSrc, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
|
import {
|
||||||
|
BRIDGE_NETWORK_NAME,
|
||||||
|
BridgeNetwork,
|
||||||
|
describeRoomBridge,
|
||||||
|
getBridgedUserNetwork,
|
||||||
|
} from '../../utils/bridge';
|
||||||
|
import { useRoomBridges } from '../../hooks/useRoomBridges';
|
||||||
|
|
||||||
|
// Discord glyph (simple-icons, 24x24 viewBox like the folds icons).
|
||||||
|
const DiscordIcon: IconSrc = () => (
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BRIDGE_ICON: Record<BridgeNetwork, IconSrc> = {
|
||||||
|
discord: DiscordIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BRIDGE_COLOR: Record<BridgeNetwork, string> = {
|
||||||
|
discord: '#5865F2',
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserBridgeBadgeProps = {
|
||||||
|
userId: string;
|
||||||
|
size?: '50' | '100' | '200';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Network icon for a user who is bridged in from another chat network, else nothing. */
|
||||||
|
export function UserBridgeBadge({ userId, size = '100' }: UserBridgeBadgeProps) {
|
||||||
|
const network = getBridgedUserNetwork(userId);
|
||||||
|
if (!network) return null;
|
||||||
|
const label = `On ${BRIDGE_NETWORK_NAME[network]} (bridged)`;
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{label}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
role="img"
|
||||||
|
aria-label={label}
|
||||||
|
style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<Icon size={size} src={BRIDGE_ICON[network]} style={{ color: BRIDGE_COLOR[network] }} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomBridgeChipProps = {
|
||||||
|
room: Room;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Header chip for a room bridged to another network; links to the channel when known. */
|
||||||
|
export function RoomBridgeChip({ room }: RoomBridgeChipProps) {
|
||||||
|
const bridges = useRoomBridges(room);
|
||||||
|
const bridge = bridges[0];
|
||||||
|
if (!bridge) return null;
|
||||||
|
const description = describeRoomBridge(bridge);
|
||||||
|
const linkProps = bridge.externalUrl
|
||||||
|
? { as: 'a' as const, href: bridge.externalUrl, target: '_blank', rel: 'noreferrer noopener' }
|
||||||
|
: {};
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Bottom"
|
||||||
|
offset={4}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text>
|
||||||
|
{bridge.externalUrl
|
||||||
|
? `${description}. Open in ${BRIDGE_NETWORK_NAME[bridge.network]}.`
|
||||||
|
: description}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<Chip
|
||||||
|
ref={triggerRef}
|
||||||
|
size="400"
|
||||||
|
variant="Secondary"
|
||||||
|
radii="300"
|
||||||
|
aria-label={description}
|
||||||
|
before={
|
||||||
|
<Icon
|
||||||
|
size="50"
|
||||||
|
src={BRIDGE_ICON[bridge.network]}
|
||||||
|
style={{ color: BRIDGE_COLOR[bridge.network] }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...linkProps}
|
||||||
|
>
|
||||||
|
<Text size="T200">{BRIDGE_NETWORK_NAME[bridge.network]}</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './BridgeBadge';
|
||||||
@@ -27,6 +27,7 @@ import { stopPropagation } from '../../utils/keyboard';
|
|||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { useLocalTime } from '../../hooks/useLocalTime';
|
import { useLocalTime } from '../../hooks/useLocalTime';
|
||||||
|
import { UserBridgeBadge } from '../bridge';
|
||||||
|
|
||||||
type UserHeroProps = {
|
type UserHeroProps = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -143,6 +144,7 @@ export function UserHeroName({
|
|||||||
>
|
>
|
||||||
{displayName ?? username ?? userId}
|
{displayName ?? username ?? userId}
|
||||||
</Text>
|
</Text>
|
||||||
|
<UserBridgeBadge userId={userId} size="200" />
|
||||||
</Box>
|
</Box>
|
||||||
{pronouns && (
|
{pronouns && (
|
||||||
<Box alignItems="Center" gap="100" style={{ marginTop: '1px', overflow: 'hidden' }}>
|
<Box alignItems="Center" gap="100" style={{ marginTop: '1px', overflow: 'hidden' }}>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/use
|
|||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useCrossSigningActive } from '../../hooks/useCrossSigning';
|
import { useCrossSigningActive } from '../../hooks/useCrossSigning';
|
||||||
import { MemberVerificationBadge } from '../../components/MemberVerificationBadge';
|
import { MemberVerificationBadge } from '../../components/MemberVerificationBadge';
|
||||||
|
import { UserBridgeBadge } from '../../components/bridge';
|
||||||
import { useUserPresence } from '../../hooks/useUserPresence';
|
import { useUserPresence } from '../../hooks/useUserPresence';
|
||||||
import { PresenceBadge, PresenceRingAvatar } from '../../components/presence';
|
import { PresenceBadge, PresenceRingAvatar } from '../../components/presence';
|
||||||
import { AvatarDecoration } from '../../components/avatar-decoration/AvatarDecoration';
|
import { AvatarDecoration } from '../../components/avatar-decoration/AvatarDecoration';
|
||||||
@@ -178,6 +179,7 @@ function MemberItem({
|
|||||||
{presence && (
|
{presence && (
|
||||||
<PresenceBadge presence={presence.presence} status={presence.status} size="300" />
|
<PresenceBadge presence={presence.presence} status={presence.status} size="300" />
|
||||||
)}
|
)}
|
||||||
|
<UserBridgeBadge userId={member.userId} />
|
||||||
{showEncryption && <MemberVerificationBadge userId={member.userId} />}
|
{showEncryption && <MemberVerificationBadge userId={member.userId} />}
|
||||||
{typing && (
|
{typing && (
|
||||||
<Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
|
<Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { PageHeader } from '../../components/page';
|
|||||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||||
|
import { RoomBridgeChip } from '../../components/bridge';
|
||||||
import { StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||||
@@ -690,6 +691,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
<RoomBridgeChip room={room} />
|
||||||
</Box>
|
</Box>
|
||||||
{topic && (
|
{topic && (
|
||||||
<UseStateProvider initial={false}>
|
<UseStateProvider initial={false}>
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
|||||||
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
|
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
|
||||||
import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room';
|
import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room';
|
||||||
import { PowerIcon } from '../../../components/power';
|
import { PowerIcon } from '../../../components/power';
|
||||||
|
import { UserBridgeBadge } from '../../../components/bridge';
|
||||||
import colorMXID from '../../../../util/colorMXID';
|
import colorMXID from '../../../../util/colorMXID';
|
||||||
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
|
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
|
||||||
import { ForwardMessageDialog } from './ForwardMessageDialog';
|
import { ForwardMessageDialog } from './ForwardMessageDialog';
|
||||||
@@ -1084,6 +1085,7 @@ export const Message = React.memo(
|
|||||||
</Text>
|
</Text>
|
||||||
</Username>
|
</Username>
|
||||||
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
|
||||||
|
<UserBridgeBadge userId={senderId} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No" gap="100">
|
<Box shrink="No" gap="100">
|
||||||
{messageLayout === MessageLayout.Modern && hover && (
|
{messageLayout === MessageLayout.Modern && hover && (
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useStateEventCallback } from './useStateEventCallback';
|
||||||
|
import { useForceUpdate } from './useForceUpdate';
|
||||||
|
import { getRoomBridges, RoomBridgeInfo } from '../utils/bridge';
|
||||||
|
import { StateEvent } from '../../types/matrix/room';
|
||||||
|
|
||||||
|
export const useRoomBridges = (room: Room): RoomBridgeInfo[] => {
|
||||||
|
const [updateCount, forceUpdate] = useForceUpdate();
|
||||||
|
|
||||||
|
useStateEventCallback(
|
||||||
|
room.client,
|
||||||
|
useCallback(
|
||||||
|
(event) => {
|
||||||
|
const type = event.getType();
|
||||||
|
if (
|
||||||
|
event.getRoomId() === room.roomId &&
|
||||||
|
(type === StateEvent.Bridge || type === StateEvent.BridgeUnstable)
|
||||||
|
) {
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[room, forceUpdate],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => getRoomBridges(room),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[room, updateCount],
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
|
import {
|
||||||
|
describeRoomBridge,
|
||||||
|
getBridgedUserNetwork,
|
||||||
|
getRoomBridges,
|
||||||
|
parseBridgeEvent,
|
||||||
|
} from './bridge';
|
||||||
|
|
||||||
|
const bridgeEvent = (type: string, stateKey: string, content: Record<string, unknown>) =>
|
||||||
|
new MatrixEvent({ type, state_key: stateKey, content, room_id: '!r:x', sender: '@discordbot:x' });
|
||||||
|
|
||||||
|
// Shape of what mautrix-discord 0.7.7 sends for a guild channel.
|
||||||
|
const discordContent = {
|
||||||
|
bridgebot: '@discordbot:matrix.lotusguild.org',
|
||||||
|
protocol: { id: 'discordgo', displayname: 'Discord' },
|
||||||
|
channel: {
|
||||||
|
id: '1565159372122882070',
|
||||||
|
displayname: '#general',
|
||||||
|
external_url: 'https://discord.com/channels/1/1565159372122882070',
|
||||||
|
},
|
||||||
|
network: { id: '1', displayname: 'Lotus Guild' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const roomWithState = (events: MatrixEvent[]): Room =>
|
||||||
|
({
|
||||||
|
getLiveTimeline: () => ({
|
||||||
|
getState: () => ({
|
||||||
|
getStateEvents: (type: string) => events.filter((e) => e.getType() === type),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}) as unknown as Room;
|
||||||
|
|
||||||
|
test('Discord ghosts are recognised; the bridge bot and lookalikes are not', () => {
|
||||||
|
assert.equal(
|
||||||
|
getBridgedUserNetwork('@discord_123456789012345678:matrix.lotusguild.org'),
|
||||||
|
'discord',
|
||||||
|
);
|
||||||
|
assert.equal(getBridgedUserNetwork('@discordbot:matrix.lotusguild.org'), undefined);
|
||||||
|
assert.equal(getBridgedUserNetwork('@discord_fan:matrix.lotusguild.org'), undefined);
|
||||||
|
assert.equal(getBridgedUserNetwork('@discord-relay:matrix.lotusguild.org'), undefined);
|
||||||
|
assert.equal(getBridgedUserNetwork('@jared:matrix.lotusguild.org'), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseBridgeEvent reads a mautrix-discord m.bridge event', () => {
|
||||||
|
assert.deepEqual(parseBridgeEvent(bridgeEvent('m.bridge', 'k', discordContent)), {
|
||||||
|
network: 'discord',
|
||||||
|
channelName: '#general',
|
||||||
|
networkName: 'Lotus Guild',
|
||||||
|
externalUrl: 'https://discord.com/channels/1/1565159372122882070',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseBridgeEvent ignores unknown protocols and empty content', () => {
|
||||||
|
assert.equal(
|
||||||
|
parseBridgeEvent(bridgeEvent('m.bridge', 'k', { protocol: { id: 'telegram' } })),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
assert.equal(parseBridgeEvent(bridgeEvent('m.bridge', 'k', {})), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only http(s) channel links are kept', () => {
|
||||||
|
const content = {
|
||||||
|
...discordContent,
|
||||||
|
channel: { displayname: '#x', external_url: ['javascript', 'alert(1)'].join(':') },
|
||||||
|
};
|
||||||
|
assert.equal(parseBridgeEvent(bridgeEvent('m.bridge', 'k', content))?.externalUrl, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRoomBridges merges m.bridge with its unstable copy, one entry per channel', () => {
|
||||||
|
const room = roomWithState([
|
||||||
|
bridgeEvent('m.bridge', 'fi.mau.discord://discord/1/2', discordContent),
|
||||||
|
bridgeEvent('uk.half-shot.bridge', 'fi.mau.discord://discord/1/2', discordContent),
|
||||||
|
]);
|
||||||
|
assert.equal(getRoomBridges(room).length, 1);
|
||||||
|
assert.equal(getRoomBridges(roomWithState([])).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('describeRoomBridge degrades gracefully without names', () => {
|
||||||
|
assert.equal(
|
||||||
|
describeRoomBridge({ network: 'discord', channelName: '#general', networkName: 'Lotus Guild' }),
|
||||||
|
'Bridged to #general in Lotus Guild on Discord',
|
||||||
|
);
|
||||||
|
assert.equal(describeRoomBridge({ network: 'discord' }), 'Bridged to Discord');
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
|
import { StateEvent } from '../../types/matrix/room';
|
||||||
|
import { getStateEvents } from './room';
|
||||||
|
|
||||||
|
export type BridgeNetwork = 'discord';
|
||||||
|
|
||||||
|
export const BRIDGE_NETWORK_NAME: Record<BridgeNetwork, string> = {
|
||||||
|
discord: 'Discord',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ghost (puppet) users a bridge creates for people on the other network.
|
||||||
|
// mautrix-discord's default username_template is discord_{{.}} with a numeric
|
||||||
|
// Discord snowflake; the bridge bot itself (@discordbot) is not a ghost.
|
||||||
|
const GHOST_PATTERNS: [RegExp, BridgeNetwork][] = [[/^@discord_\d+:/, 'discord']];
|
||||||
|
|
||||||
|
export const getBridgedUserNetwork = (userId: string): BridgeNetwork | undefined =>
|
||||||
|
GHOST_PATTERNS.find(([pattern]) => pattern.test(userId))?.[1];
|
||||||
|
|
||||||
|
// m.bridge protocol.id values that mean each network.
|
||||||
|
const PROTOCOL_NETWORKS: Record<string, BridgeNetwork> = {
|
||||||
|
discordgo: 'discord',
|
||||||
|
discord: 'discord',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RoomBridgeInfo = {
|
||||||
|
network: BridgeNetwork;
|
||||||
|
channelName?: string;
|
||||||
|
networkName?: string;
|
||||||
|
externalUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asString = (value: unknown): string | undefined =>
|
||||||
|
typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
||||||
|
|
||||||
|
// Only http(s) links are made clickable; the bridge (or anyone with state
|
||||||
|
// power in the room) controls this value.
|
||||||
|
const safeExternalUrl = (value: unknown): string | undefined => {
|
||||||
|
const raw = asString(value);
|
||||||
|
if (!raw) return undefined;
|
||||||
|
try {
|
||||||
|
const url = new URL(raw);
|
||||||
|
return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBridgeEvent = (event: MatrixEvent): RoomBridgeInfo | undefined => {
|
||||||
|
const content = event.getContent();
|
||||||
|
const protocolId = asString(content?.protocol?.id)?.toLowerCase();
|
||||||
|
const network = protocolId ? PROTOCOL_NETWORKS[protocolId] : undefined;
|
||||||
|
if (!network) return undefined;
|
||||||
|
return {
|
||||||
|
network,
|
||||||
|
channelName: asString(content.channel?.displayname),
|
||||||
|
networkName: asString(content.network?.displayname),
|
||||||
|
externalUrl: safeExternalUrl(content.channel?.external_url),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Known bridges in a room, one per bridged channel (m.bridge preferred over the unstable copy). */
|
||||||
|
export const getRoomBridges = (room: Room): RoomBridgeInfo[] => {
|
||||||
|
const byKey = new Map<string, RoomBridgeInfo>();
|
||||||
|
[StateEvent.BridgeUnstable, StateEvent.Bridge].forEach((type) => {
|
||||||
|
getStateEvents(room, type).forEach((event) => {
|
||||||
|
const info = parseBridgeEvent(event);
|
||||||
|
if (info) byKey.set(event.getStateKey() ?? '', info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(byKey.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const describeRoomBridge = (info: RoomBridgeInfo): string => {
|
||||||
|
const network = BRIDGE_NETWORK_NAME[info.network];
|
||||||
|
if (info.channelName && info.networkName) {
|
||||||
|
return `Bridged to ${info.channelName} in ${info.networkName} on ${network}`;
|
||||||
|
}
|
||||||
|
if (info.channelName) return `Bridged to ${info.channelName} on ${network}`;
|
||||||
|
return `Bridged to ${network}`;
|
||||||
|
};
|
||||||
@@ -50,6 +50,10 @@ export enum StateEvent {
|
|||||||
// [P5-15 v2] Room/Space soundboard pack (mirrors PoniesRoomEmotes). Per
|
// [P5-15 v2] Room/Space soundboard pack (mirrors PoniesRoomEmotes). Per
|
||||||
// state-key, aggregated with parent-space packs like custom emoji.
|
// state-key, aggregated with parent-space packs like custom emoji.
|
||||||
LotusSoundboardRoom = 'io.lotus.soundboard',
|
LotusSoundboardRoom = 'io.lotus.soundboard',
|
||||||
|
// MSC2346 bridge info; state_key identifies the bridged channel. Bridges
|
||||||
|
// still send the pre-stable uk.half-shot.bridge copy alongside it.
|
||||||
|
Bridge = 'm.bridge',
|
||||||
|
BridgeUnstable = 'uk.half-shot.bridge',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MessageEvent {
|
export enum MessageEvent {
|
||||||
|
|||||||
Reference in New Issue
Block a user