mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/rightpanel: implement member list
This commit is contained in:
parent
24f2e3722d
commit
04117b5211
12 changed files with 182 additions and 29 deletions
|
@ -36,6 +36,13 @@ export function useRoomState(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRoomMembers(room?: RoomStateStore): MemDBEvent[] {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
room ? room.stateSubs.getSubscriber("m.room.member") : noopSubscribe,
|
||||||
|
room ? room.getMembers : () => [],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const noopSubscribe = () => () => {}
|
const noopSubscribe = () => () => {}
|
||||||
const returnNull = () => null
|
const returnNull = () => null
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
import { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji"
|
import { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji"
|
||||||
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||||
import Subscribable, { MultiSubscribable } from "@/util/subscribable.ts"
|
import Subscribable, { MultiSubscribable } from "@/util/subscribable.ts"
|
||||||
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
import {
|
import {
|
||||||
DBRoom,
|
DBRoom,
|
||||||
EncryptedEventContent,
|
EncryptedEventContent,
|
||||||
|
@ -26,10 +27,13 @@ import {
|
||||||
ImagePack,
|
ImagePack,
|
||||||
LazyLoadSummary,
|
LazyLoadSummary,
|
||||||
MemDBEvent,
|
MemDBEvent,
|
||||||
|
MemberEventContent,
|
||||||
|
PowerLevelEventContent,
|
||||||
RawDBEvent,
|
RawDBEvent,
|
||||||
RoomID,
|
RoomID,
|
||||||
SyncRoom,
|
SyncRoom,
|
||||||
TimelineRowTuple,
|
TimelineRowTuple,
|
||||||
|
UserID,
|
||||||
roomStateGUIDToString,
|
roomStateGUIDToString,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import type { StateStore } from "./main.ts"
|
import type { StateStore } from "./main.ts"
|
||||||
|
@ -80,6 +84,7 @@ export class RoomStateStore {
|
||||||
readonly requestedEvents: Set<EventID> = new Set()
|
readonly requestedEvents: Set<EventID> = new Set()
|
||||||
readonly openNotifications: Map<EventRowID, Notification> = new Map()
|
readonly openNotifications: Map<EventRowID, Notification> = new Map()
|
||||||
readonly emojiPacks: Map<string, CustomEmojiPack | null> = new Map()
|
readonly emojiPacks: Map<string, CustomEmojiPack | null> = new Map()
|
||||||
|
#membersCache: MemDBEvent[] | null = null
|
||||||
#allPacksCache: Record<string, CustomEmojiPack> | null = null
|
#allPacksCache: Record<string, CustomEmojiPack> | null = null
|
||||||
readonly pendingEvents: EventRowID[] = []
|
readonly pendingEvents: EventRowID[] = []
|
||||||
paginating = false
|
paginating = false
|
||||||
|
@ -150,6 +155,36 @@ export class RoomStateStore {
|
||||||
return this.#allPacksCache
|
return this.#allPacksCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMembers = (): MemDBEvent[] => {
|
||||||
|
if (this.#membersCache === null) {
|
||||||
|
const memberEvtIDs = this.state.get("m.room.member")
|
||||||
|
if (!memberEvtIDs) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const powerLevels: PowerLevelEventContent = this.getStateEvent("m.room.power_levels", "")?.content ?? {}
|
||||||
|
this.#membersCache = memberEvtIDs.values()
|
||||||
|
.map(rowID => this.eventsByRowID.get(rowID))
|
||||||
|
.filter(evt => !!evt)
|
||||||
|
.toArray()
|
||||||
|
this.#membersCache.sort((a, b) => {
|
||||||
|
const aUserID = a.state_key as UserID
|
||||||
|
const bUserID = b.state_key as UserID
|
||||||
|
const aPower = powerLevels.users?.[aUserID] ?? powerLevels.users_default ?? 0
|
||||||
|
const bPower = powerLevels.users?.[bUserID] ?? powerLevels.users_default ?? 0
|
||||||
|
if (aPower !== bPower) {
|
||||||
|
return bPower - aPower
|
||||||
|
}
|
||||||
|
const aName = getDisplayname(aUserID, a.content as MemberEventContent).toLowerCase()
|
||||||
|
const bName = getDisplayname(bUserID, b.content as MemberEventContent).toLowerCase()
|
||||||
|
if (aName === bName) {
|
||||||
|
return aUserID.localeCompare(bUserID)
|
||||||
|
}
|
||||||
|
return aName.localeCompare(bName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return this.#membersCache
|
||||||
|
}
|
||||||
|
|
||||||
getPinnedEvents(): EventID[] {
|
getPinnedEvents(): EventID[] {
|
||||||
const pinnedList = this.getStateEvent("m.room.pinned_events", "")?.content?.pinned
|
const pinnedList = this.getStateEvent("m.room.pinned_events", "")?.content?.pinned
|
||||||
if (Array.isArray(pinnedList)) {
|
if (Array.isArray(pinnedList)) {
|
||||||
|
@ -229,6 +264,8 @@ export class RoomStateStore {
|
||||||
this.emojiPacks.delete(key)
|
this.emojiPacks.delete(key)
|
||||||
this.#allPacksCache = null
|
this.#allPacksCache = null
|
||||||
this.parent.invalidateEmojiPacksCache()
|
this.parent.invalidateEmojiPacksCache()
|
||||||
|
} else if (evtType === "m.room.member" || evtType === "m.room.power_levels") {
|
||||||
|
this.#membersCache = null
|
||||||
}
|
}
|
||||||
this.stateSubs.notify(this.stateSubKey(evtType, key))
|
this.stateSubs.notify(this.stateSubKey(evtType, key))
|
||||||
}
|
}
|
||||||
|
|
71
web/src/ui/rightpanel/MemberList.tsx
Normal file
71
web/src/ui/rightpanel/MemberList.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import { use, useCallback, useState } from "react"
|
||||||
|
import { getAvatarURL } from "@/api/media.ts"
|
||||||
|
import { useRoomMembers } from "@/api/statestore"
|
||||||
|
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
|
import { RoomContext } from "../roomview/roomcontext.ts"
|
||||||
|
|
||||||
|
interface MemberRowProps {
|
||||||
|
evt: MemDBEvent
|
||||||
|
onClick: (evt: React.MouseEvent<HTMLDivElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberRow = ({ evt, onClick }: MemberRowProps) => {
|
||||||
|
const userID = evt.state_key!
|
||||||
|
const content = evt.content as MemberEventContent
|
||||||
|
return <div className="member" data-target-panel="user" data-target-user={userID} onClick={onClick}>
|
||||||
|
<img
|
||||||
|
className="avatar"
|
||||||
|
src={getAvatarURL(userID, content)}
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<span className="displayname">{getDisplayname(userID, content)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberList = () => {
|
||||||
|
const [limit, setLimit] = useState(50)
|
||||||
|
const increaseLimit = useCallback(() => setLimit(limit => limit + 50), [])
|
||||||
|
const roomCtx = use(RoomContext)
|
||||||
|
const memberEvents = useRoomMembers(roomCtx?.store)
|
||||||
|
if (!roomCtx) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const mainScreen = use(MainScreenContext)
|
||||||
|
const members = []
|
||||||
|
for (const evt of memberEvents) {
|
||||||
|
members.push(<MemberRow
|
||||||
|
key={evt.state_key}
|
||||||
|
evt={evt}
|
||||||
|
onClick={mainScreen.clickRightPanelOpener}
|
||||||
|
/>)
|
||||||
|
if (members.length >= limit) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
{members}
|
||||||
|
{memberEvents.length > limit ? <button onClick={increaseLimit}>
|
||||||
|
and {memberEvents.length - limit} others…
|
||||||
|
</button> : null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MemberList
|
|
@ -89,3 +89,36 @@ div.right-panel-content.user {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.right-panel-content.members {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> div.member {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
cursor: var(--clickable-cursor);
|
||||||
|
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-height: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
padding: .25rem;
|
||||||
|
|
||||||
|
> span.displayname {
|
||||||
|
overflow: hidden;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
background-color: var(--light-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
import { JSX, use } from "react"
|
import { JSX, use } from "react"
|
||||||
import type { UserID } from "@/api/types"
|
import type { UserID } from "@/api/types"
|
||||||
import MainScreenContext from "../MainScreenContext.ts"
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
|
import MemberList from "./MemberList.tsx"
|
||||||
import PinnedMessages from "./PinnedMessages.tsx"
|
import PinnedMessages from "./PinnedMessages.tsx"
|
||||||
import UserInfo from "./UserInfo.tsx"
|
import UserInfo from "./UserInfo.tsx"
|
||||||
import BackIcon from "@/icons/back.svg?react"
|
import BackIcon from "@/icons/back.svg?react"
|
||||||
|
@ -51,7 +52,7 @@ function renderRightPanelContent(props: RightPanelProps): JSX.Element | null {
|
||||||
case "pinned-messages":
|
case "pinned-messages":
|
||||||
return <PinnedMessages />
|
return <PinnedMessages />
|
||||||
case "members":
|
case "members":
|
||||||
return <>Member list is not yet implemented</>
|
return <MemberList />
|
||||||
case "user":
|
case "user":
|
||||||
return <UserInfo userID={props.userID} />
|
return <UserInfo userID={props.userID} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { PuffLoader } from "react-spinners"
|
||||||
import { getAvatarURL } from "@/api/media.ts"
|
import { getAvatarURL } from "@/api/media.ts"
|
||||||
import { useRoomState } from "@/api/statestore"
|
import { useRoomState } from "@/api/statestore"
|
||||||
import { MemberEventContent, UserID, UserProfile } from "@/api/types"
|
import { MemberEventContent, UserID, UserProfile } from "@/api/types"
|
||||||
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import { LightboxContext, OpenLightboxType } from "../modal/Lightbox.tsx"
|
import { LightboxContext, OpenLightboxType } from "../modal/Lightbox.tsx"
|
||||||
import { RoomContext } from "../roomview/roomcontext.ts"
|
import { RoomContext } from "../roomview/roomcontext.ts"
|
||||||
|
@ -56,31 +57,22 @@ interface RenderUserInfoParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUserInfo({ userID, profile, error, openLightbox }: RenderUserInfoParams) {
|
function renderUserInfo({ userID, profile, error, openLightbox }: RenderUserInfoParams) {
|
||||||
const displayname = profile?.displayname ?? userID
|
const displayname = getDisplayname(userID, profile)
|
||||||
if (profile === null) {
|
|
||||||
return <>
|
return <>
|
||||||
<div className="avatar-container">
|
<div className="avatar-container">
|
||||||
<PuffLoader
|
{profile === null && error === null ? <PuffLoader
|
||||||
color="var(--primary-color)"
|
color="var(--primary-color)"
|
||||||
size="100%"
|
size="100%"
|
||||||
className="avatar-loader"
|
className="avatar-loader"
|
||||||
/>
|
/> : <img
|
||||||
</div>
|
|
||||||
<div className="displayname"> </div>
|
|
||||||
<div className="userid">{userID}</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
return <>
|
|
||||||
<div className="avatar-container">
|
|
||||||
<img
|
|
||||||
className="avatar"
|
className="avatar"
|
||||||
src={getAvatarURL(userID, profile)}
|
src={getAvatarURL(userID, profile)}
|
||||||
onClick={openLightbox}
|
onClick={openLightbox}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>}
|
||||||
</div>
|
</div>
|
||||||
<div className="displayname" title={displayname}>{displayname}</div>
|
<div className="displayname" title={displayname}>{displayname}</div>
|
||||||
{displayname !== userID ? <div className="userid" title={userID}>{userID}</div> : null}
|
<div className="userid" title={userID}>{userID}</div>
|
||||||
{error && <div className="error">{`${error}`}</div>}
|
{error && <div className="error">{`${error}`}</div>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { getAvatarURL } from "@/api/media.ts"
|
||||||
import type { RoomListEntry } from "@/api/statestore"
|
import type { RoomListEntry } from "@/api/statestore"
|
||||||
import type { MemDBEvent, MemberEventContent } from "@/api/types"
|
import type { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
import useContentVisibility from "@/util/contentvisibility.ts"
|
import useContentVisibility from "@/util/contentvisibility.ts"
|
||||||
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import MainScreenContext from "../MainScreenContext.ts"
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
|
|
||||||
|
@ -33,12 +34,9 @@ function usePreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null):
|
||||||
}
|
}
|
||||||
if ((evt.type === "m.room.message" || evt.type === "m.sticker") && typeof evt.content.body === "string") {
|
if ((evt.type === "m.room.message" || evt.type === "m.sticker") && typeof evt.content.body === "string") {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
let displayname = (senderMemberEvt?.content as MemberEventContent)?.displayname
|
const displayname = evt.sender === client.userID
|
||||||
if (evt.sender === client.userID) {
|
? "You"
|
||||||
displayname = "You"
|
: getDisplayname(evt.sender, senderMemberEvt?.content as MemberEventContent)
|
||||||
} else if (!displayname) {
|
|
||||||
displayname = evt.sender.slice(1).split(":")[0]
|
|
||||||
}
|
|
||||||
let previewText = evt.content.body
|
let previewText = evt.content.body
|
||||||
if (evt.content.formatted_body?.includes?.("data-mx-spoiler")) {
|
if (evt.content.formatted_body?.includes?.("data-mx-spoiler")) {
|
||||||
previewText = "<message contains spoilers>"
|
previewText = "<message contains spoilers>"
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { use } from "react"
|
||||||
import { getAvatarURL, getUserColorIndex } from "@/api/media.ts"
|
import { getAvatarURL, getUserColorIndex } from "@/api/media.ts"
|
||||||
import { RoomStateStore, useRoomEvent, useRoomState } from "@/api/statestore"
|
import { RoomStateStore, useRoomEvent, useRoomState } from "@/api/statestore"
|
||||||
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import { ContentErrorBoundary, getBodyType } from "./content"
|
import { ContentErrorBoundary, getBodyType } from "./content"
|
||||||
import CloseButton from "@/icons/close.svg?react"
|
import CloseButton from "@/icons/close.svg?react"
|
||||||
|
@ -94,7 +95,7 @@ export const ReplyBody = ({ room, event, onClose, isThread, isEditing }: ReplyBo
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={`event-sender sender-color-${userColorIndex}`}>
|
<span className={`event-sender sender-color-${userColorIndex}`}>
|
||||||
{memberEvtContent?.displayname || event.sender}
|
{getDisplayname(event.sender, memberEvtContent)}
|
||||||
</span>
|
</span>
|
||||||
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
|
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import React, { use, useState } from "react"
|
||||||
import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
|
import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
|
||||||
import { useRoomState } from "@/api/statestore"
|
import { useRoomState } from "@/api/statestore"
|
||||||
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||||
import { isEventID } from "@/util/validation.ts"
|
import { getDisplayname, isEventID } from "@/util/validation.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import MainScreenContext from "../MainScreenContext.ts"
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||||
|
@ -125,7 +125,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
|
||||||
data-target-user={evt.sender}
|
data-target-user={evt.sender}
|
||||||
onClick={roomCtx.appendMentionToComposer}
|
onClick={roomCtx.appendMentionToComposer}
|
||||||
>
|
>
|
||||||
{memberEvtContent?.displayname || evt.sender}
|
{getDisplayname(evt.sender, memberEvtContent)}
|
||||||
</span>
|
</span>
|
||||||
<span className="event-time" title={fullTime}>{shortTime}</span>
|
<span className="event-time" title={fullTime}>{shortTime}</span>
|
||||||
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
|
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { MessageEventContent } from "@/api/types"
|
import { MessageEventContent } from "@/api/types"
|
||||||
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
import EventContentProps from "./props.ts"
|
import EventContentProps from "./props.ts"
|
||||||
|
|
||||||
function isImageElement(elem: EventTarget): elem is HTMLImageElement {
|
function isImageElement(elem: EventTarget): elem is HTMLImageElement {
|
||||||
|
@ -81,7 +82,7 @@ const TextMessageBody = ({ event, sender }: EventContentProps) => {
|
||||||
classNames.push("notice-message")
|
classNames.push("notice-message")
|
||||||
} else if (content.msgtype === "m.emote") {
|
} else if (content.msgtype === "m.emote") {
|
||||||
classNames.push("emote-message")
|
classNames.push("emote-message")
|
||||||
eventSenderName = sender?.content?.displayname || event.sender
|
eventSenderName = getDisplayname(event.sender, sender?.content)
|
||||||
}
|
}
|
||||||
if (event.local_content?.big_emoji) {
|
if (event.local_content?.big_emoji) {
|
||||||
classNames.push("big-emoji-body")
|
classNames.push("big-emoji-body")
|
||||||
|
|
|
@ -24,4 +24,7 @@ if (!window.Iterator?.prototype.map) {
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
Array.prototype.toArray = function() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { ContentURI, EventID, RoomAlias, RoomID, UserID } from "@/api/types"
|
import { ContentURI, EventID, RoomAlias, RoomID, UserID, UserProfile } from "@/api/types"
|
||||||
|
|
||||||
const simpleHomeserverRegex = /^[a-zA-Z0-9.:-]+$/
|
const simpleHomeserverRegex = /^[a-zA-Z0-9.:-]+$/
|
||||||
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
|
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
|
||||||
|
@ -39,6 +39,15 @@ export const isRoomID = (roomID: unknown) => isIdentifier<RoomID>(roomID, "!", t
|
||||||
export const isRoomAlias = (roomAlias: unknown) => isIdentifier<RoomAlias>(roomAlias, "#", true)
|
export const isRoomAlias = (roomAlias: unknown) => isIdentifier<RoomAlias>(roomAlias, "#", true)
|
||||||
export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc)
|
export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc)
|
||||||
|
|
||||||
|
export function getLocalpart(userID: UserID): string {
|
||||||
|
const idx = userID.indexOf(":")
|
||||||
|
return idx > 0 ? userID.slice(1, idx) : userID.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayname(userID: UserID, profile?: UserProfile | null): string {
|
||||||
|
return profile?.displayname || getLocalpart(userID)
|
||||||
|
}
|
||||||
|
|
||||||
export function parseMXC(mxc: unknown): [string, string] | [] {
|
export function parseMXC(mxc: unknown): [string, string] | [] {
|
||||||
if (typeof mxc !== "string") {
|
if (typeof mxc !== "string") {
|
||||||
return []
|
return []
|
||||||
|
|
Loading…
Add table
Reference in a new issue