mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
parent
b30025746d
commit
e0f107f028
10 changed files with 251 additions and 72 deletions
|
@ -39,7 +39,7 @@ import {
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import { InvitedRoomStore } from "./invitedroom.ts"
|
import { InvitedRoomStore } from "./invitedroom.ts"
|
||||||
import { RoomStateStore } from "./room.ts"
|
import { RoomStateStore } from "./room.ts"
|
||||||
import { RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace } from "./space.ts"
|
import { DirectChatSpace, RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts"
|
||||||
|
|
||||||
export interface RoomListEntry {
|
export interface RoomListEntry {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
|
@ -76,6 +76,13 @@ export class StateStore {
|
||||||
readonly topLevelSpaces = new NonNullCachedEventDispatcher<RoomID[]>([])
|
readonly topLevelSpaces = new NonNullCachedEventDispatcher<RoomID[]>([])
|
||||||
readonly spaceEdges: Map<RoomID, SpaceEdgeStore> = new Map()
|
readonly spaceEdges: Map<RoomID, SpaceEdgeStore> = new Map()
|
||||||
readonly spaceOrphans = new SpaceOrphansSpace(this)
|
readonly spaceOrphans = new SpaceOrphansSpace(this)
|
||||||
|
readonly directChatsSpace = new DirectChatSpace()
|
||||||
|
readonly unreadsSpace = new UnreadsSpace()
|
||||||
|
readonly pseudoSpaces = [
|
||||||
|
this.spaceOrphans,
|
||||||
|
this.directChatsSpace,
|
||||||
|
this.unreadsSpace,
|
||||||
|
] as const
|
||||||
currentRoomListQuery: string = ""
|
currentRoomListQuery: string = ""
|
||||||
currentRoomListFilter: RoomListFilter | null = null
|
currentRoomListFilter: RoomListFilter | null = null
|
||||||
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
||||||
|
@ -177,6 +184,25 @@ export class StateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#applyUnreadModification(meta: RoomListEntry | null, oldMeta: RoomListEntry | undefined | null) {
|
||||||
|
const someMeta = meta ?? oldMeta
|
||||||
|
if (!someMeta) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.spaceOrphans.include(someMeta)) {
|
||||||
|
this.spaceOrphans.applyUnreads(meta, oldMeta)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.directChatsSpace.include(someMeta)) {
|
||||||
|
this.directChatsSpace.applyUnreads(meta, oldMeta)
|
||||||
|
}
|
||||||
|
for (const space of this.spaceEdges.values()) {
|
||||||
|
if (space.include(someMeta)) {
|
||||||
|
space.applyUnreads(meta, oldMeta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applySync(sync: SyncCompleteData) {
|
applySync(sync: SyncCompleteData) {
|
||||||
if (sync.clear_state && this.rooms.size > 0) {
|
if (sync.clear_state && this.rooms.size > 0) {
|
||||||
console.info("Clearing state store as sync told to reset and there are rooms in the store")
|
console.info("Clearing state store as sync told to reset and there are rooms in the store")
|
||||||
|
@ -186,9 +212,11 @@ export class StateStore {
|
||||||
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
||||||
for (const data of sync.invited_rooms ?? []) {
|
for (const data of sync.invited_rooms ?? []) {
|
||||||
const room = new InvitedRoomStore(data, this)
|
const room = new InvitedRoomStore(data, this)
|
||||||
|
const oldEntry = this.inviteRooms.get(room.room_id)
|
||||||
this.inviteRooms.set(room.room_id, room)
|
this.inviteRooms.set(room.room_id, room)
|
||||||
if (!resyncRoomList) {
|
if (!resyncRoomList) {
|
||||||
changedRoomListEntries.set(room.room_id, room)
|
changedRoomListEntries.set(room.room_id, room)
|
||||||
|
this.#applyUnreadModification(room, oldEntry)
|
||||||
}
|
}
|
||||||
if (this.activeRoomID === room.room_id) {
|
if (this.activeRoomID === room.room_id) {
|
||||||
this.switchRoom?.(room.room_id)
|
this.switchRoom?.(room.room_id)
|
||||||
|
@ -209,7 +237,10 @@ export class StateStore {
|
||||||
const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room))
|
const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room))
|
||||||
room.applySync(data)
|
room.applySync(data)
|
||||||
if (roomListEntryChanged) {
|
if (roomListEntryChanged) {
|
||||||
changedRoomListEntries.set(roomID, this.#makeRoomListEntry(data, room))
|
const entry = this.#makeRoomListEntry(data, room)
|
||||||
|
changedRoomListEntries.set(roomID, entry)
|
||||||
|
this.#applyUnreadModification(entry, room.roomListEntry)
|
||||||
|
room.roomListEntry = entry
|
||||||
}
|
}
|
||||||
if (!resyncRoomList) {
|
if (!resyncRoomList) {
|
||||||
// When we join a valid replacement room, hide the tombstoned room.
|
// When we join a valid replacement room, hide the tombstoned room.
|
||||||
|
@ -256,6 +287,10 @@ export class StateStore {
|
||||||
.map(entry => this.#makeRoomListEntry(entry))
|
.map(entry => this.#makeRoomListEntry(entry))
|
||||||
.filter(entry => entry !== null))
|
.filter(entry => entry !== null))
|
||||||
updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp)
|
updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp)
|
||||||
|
for (const entry of updatedRoomList) {
|
||||||
|
this.#applyUnreadModification(entry, undefined)
|
||||||
|
this.rooms.get(entry.room_id)!.roomListEntry = entry
|
||||||
|
}
|
||||||
} else if (changedRoomListEntries.size > 0) {
|
} else if (changedRoomListEntries.size > 0) {
|
||||||
updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id))
|
updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id))
|
||||||
for (const entry of changedRoomListEntries.values()) {
|
for (const entry of changedRoomListEntries.values()) {
|
||||||
|
|
|
@ -42,7 +42,7 @@ import {
|
||||||
UserID,
|
UserID,
|
||||||
roomStateGUIDToString,
|
roomStateGUIDToString,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import type { StateStore } from "./main.ts"
|
import type { RoomListEntry, StateStore } from "./main.ts"
|
||||||
|
|
||||||
function arraysAreEqual<T>(arr1?: T[], arr2?: T[]): boolean {
|
function arraysAreEqual<T>(arr1?: T[], arr2?: T[]): boolean {
|
||||||
if (!arr1 || !arr2) {
|
if (!arr1 || !arr2) {
|
||||||
|
@ -126,6 +126,7 @@ export class RoomStateStore {
|
||||||
readUpToRow = -1
|
readUpToRow = -1
|
||||||
hasMoreHistory = true
|
hasMoreHistory = true
|
||||||
hidden = false
|
hidden = false
|
||||||
|
roomListEntry: RoomListEntry | undefined | null
|
||||||
|
|
||||||
constructor(meta: DBRoom, private parent: StateStore) {
|
constructor(meta: DBRoom, private parent: StateStore) {
|
||||||
this.roomID = meta.room_id
|
this.roomID = meta.room_id
|
||||||
|
|
|
@ -15,26 +15,79 @@
|
||||||
// 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 { RoomListEntry, StateStore } from "@/api/statestore/main.ts"
|
import { RoomListEntry, StateStore } from "@/api/statestore/main.ts"
|
||||||
import { DBSpaceEdge, RoomID } from "@/api/types"
|
import { DBSpaceEdge, RoomID } from "@/api/types"
|
||||||
|
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||||
|
|
||||||
export interface RoomListFilter {
|
export interface RoomListFilter {
|
||||||
id: string
|
id: string
|
||||||
include(room: RoomListEntry): boolean
|
include(room: RoomListEntry): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DirectChatSpace: RoomListFilter = {
|
export interface SpaceUnreadCounts {
|
||||||
id: "fi.mau.gomuks.direct_chats",
|
unread_messages: number
|
||||||
include: room => !!room.dm_user_id,
|
unread_notifications: number
|
||||||
|
unread_highlights: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnreadsSpace: RoomListFilter = {
|
const emptyUnreadCounts: SpaceUnreadCounts = {
|
||||||
id: "fi.mau.gomuks.unreads",
|
unread_messages: 0,
|
||||||
include: room => Boolean(room.unread_messages
|
unread_notifications: 0,
|
||||||
|
unread_highlights: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Space implements RoomListFilter {
|
||||||
|
counts = new NonNullCachedEventDispatcher(emptyUnreadCounts)
|
||||||
|
|
||||||
|
abstract id: string
|
||||||
|
abstract include(room: RoomListEntry): boolean
|
||||||
|
|
||||||
|
applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) {
|
||||||
|
const mergedCounts: SpaceUnreadCounts = {
|
||||||
|
unread_messages: this.counts.current.unread_messages
|
||||||
|
+ (newCounts?.unread_messages ?? 0) - (oldCounts?.unread_messages ?? 0),
|
||||||
|
unread_notifications: this.counts.current.unread_notifications
|
||||||
|
+ (newCounts?.unread_notifications ?? 0) - (oldCounts?.unread_notifications ?? 0),
|
||||||
|
unread_highlights: this.counts.current.unread_highlights
|
||||||
|
+ (newCounts?.unread_highlights ?? 0) - (oldCounts?.unread_highlights ?? 0),
|
||||||
|
}
|
||||||
|
if (mergedCounts.unread_messages < 0) {
|
||||||
|
mergedCounts.unread_messages = 0
|
||||||
|
}
|
||||||
|
if (mergedCounts.unread_notifications < 0) {
|
||||||
|
mergedCounts.unread_notifications = 0
|
||||||
|
}
|
||||||
|
if (mergedCounts.unread_highlights < 0) {
|
||||||
|
mergedCounts.unread_highlights = 0
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
mergedCounts.unread_messages !== this.counts.current.unread_messages
|
||||||
|
|| mergedCounts.unread_notifications !== this.counts.current.unread_notifications
|
||||||
|
|| mergedCounts.unread_highlights !== this.counts.current.unread_highlights
|
||||||
|
) {
|
||||||
|
this.counts.emit(mergedCounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DirectChatSpace extends Space {
|
||||||
|
id = "fi.mau.gomuks.direct_chats"
|
||||||
|
|
||||||
|
include(room: RoomListEntry): boolean {
|
||||||
|
return Boolean(room.dm_user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnreadsSpace extends Space {
|
||||||
|
id = "fi.mau.gomuks.unreads"
|
||||||
|
|
||||||
|
include(room: RoomListEntry): boolean {
|
||||||
|
return Boolean(room.unread_messages
|
||||||
|| room.unread_notifications
|
|| room.unread_notifications
|
||||||
|| room.unread_highlights
|
|| room.unread_highlights
|
||||||
|| room.marked_unread),
|
|| room.marked_unread)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SpaceEdgeStore implements RoomListFilter {
|
export class SpaceEdgeStore extends Space {
|
||||||
#children: DBSpaceEdge[] = []
|
#children: DBSpaceEdge[] = []
|
||||||
#childRooms: Set<RoomID> = new Set()
|
#childRooms: Set<RoomID> = new Set()
|
||||||
#flattenedRooms: Set<RoomID> = new Set()
|
#flattenedRooms: Set<RoomID> = new Set()
|
||||||
|
@ -42,6 +95,7 @@ export class SpaceEdgeStore implements RoomListFilter {
|
||||||
readonly #parentSpaces: Set<SpaceEdgeStore> = new Set()
|
readonly #parentSpaces: Set<SpaceEdgeStore> = new Set()
|
||||||
|
|
||||||
constructor(public id: RoomID, private parent: StateStore) {
|
constructor(public id: RoomID, private parent: StateStore) {
|
||||||
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
#addParent(parent: SpaceEdgeStore) {
|
#addParent(parent: SpaceEdgeStore) {
|
||||||
|
|
|
@ -55,6 +55,9 @@
|
||||||
--unread-counter-notification-bg: rgba(50, 150, 0, 0.7);
|
--unread-counter-notification-bg: rgba(50, 150, 0, 0.7);
|
||||||
--unread-counter-marked-unread-bg: var(--unread-counter-notification-bg);
|
--unread-counter-marked-unread-bg: var(--unread-counter-notification-bg);
|
||||||
--unread-counter-highlight-bg: rgba(200, 0, 0, 0.7);
|
--unread-counter-highlight-bg: rgba(200, 0, 0, 0.7);
|
||||||
|
--space-unread-counter-message-bg: rgb(100, 100, 100, 0.9);
|
||||||
|
--space-unread-counter-notification-bg: rgb(50, 150, 0);
|
||||||
|
--space-unread-counter-highlight-bg: rgb(200, 0, 0);
|
||||||
|
|
||||||
--sender-color-0: #a4041d;
|
--sender-color-0: #a4041d;
|
||||||
--sender-color-1: #9b2200;
|
--sender-color-1: #9b2200;
|
||||||
|
@ -136,6 +139,9 @@
|
||||||
--unread-counter-message-bg: rgba(255, 255, 255, 0.5);
|
--unread-counter-message-bg: rgba(255, 255, 255, 0.5);
|
||||||
--unread-counter-notification-bg: rgba(150, 255, 0, 0.7);
|
--unread-counter-notification-bg: rgba(150, 255, 0, 0.7);
|
||||||
--unread-counter-highlight-bg: rgba(255, 50, 50, 0.7);
|
--unread-counter-highlight-bg: rgba(255, 50, 50, 0.7);
|
||||||
|
--space-unread-counter-message-bg: rgb(200, 200, 200, 0.8);
|
||||||
|
--space-unread-counter-notification-bg: rgb(150, 255, 0);
|
||||||
|
--space-unread-counter-highlight-bg: rgb(255, 50, 50);
|
||||||
|
|
||||||
--sender-color-0: #ff877c;
|
--sender-color-0: #ff877c;
|
||||||
--sender-color-1: #f6913d;
|
--sender-color-1: #f6913d;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import useContentVisibility from "@/util/contentvisibility.ts"
|
||||||
import { getDisplayname } from "@/util/validation.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"
|
||||||
|
import UnreadCount from "./UnreadCount.tsx"
|
||||||
|
|
||||||
export interface RoomListEntryProps {
|
export interface RoomListEntryProps {
|
||||||
room: RoomListEntry
|
room: RoomListEntry
|
||||||
|
@ -56,14 +57,6 @@ function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null):
|
||||||
|
|
||||||
function renderEntry(room: RoomListEntry) {
|
function renderEntry(room: RoomListEntry) {
|
||||||
const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender)
|
const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender)
|
||||||
const unreadCount = room.unread_messages || room.unread_notifications || room.unread_highlights
|
|
||||||
const countIsBig = Boolean(room.unread_notifications || room.unread_highlights)
|
|
||||||
let unreadCountDisplay = unreadCount.toString()
|
|
||||||
if (unreadCount > 999 && countIsBig) {
|
|
||||||
unreadCountDisplay = "99+"
|
|
||||||
} else if (unreadCount > 9999 && countIsBig) {
|
|
||||||
unreadCountDisplay = "999+"
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className="room-entry-left">
|
<div className="room-entry-left">
|
||||||
|
@ -78,15 +71,7 @@ function renderEntry(room: RoomListEntry) {
|
||||||
<div className="room-name">{room.name}</div>
|
<div className="room-name">{room.name}</div>
|
||||||
{previewText && <div className="message-preview" title={previewText}>{croppedPreviewText}</div>}
|
{previewText && <div className="message-preview" title={previewText}>{croppedPreviewText}</div>}
|
||||||
</div>
|
</div>
|
||||||
{(room.unread_messages || room.marked_unread) ? <div className="room-entry-unreads">
|
<UnreadCount counts={room} />
|
||||||
<div title={unreadCount.toString()} className={`unread-count ${
|
|
||||||
room.marked_unread ? "marked-unread" : ""} ${
|
|
||||||
room.unread_notifications ? "notified" : ""} ${
|
|
||||||
room.unread_highlights ? "highlighted" : ""}`}
|
|
||||||
>
|
|
||||||
{unreadCountDisplay}
|
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
// 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 { JSX } from "react"
|
import { JSX } from "react"
|
||||||
import type { RoomListFilter } from "@/api/statestore/space.ts"
|
import { RoomListFilter, Space } from "@/api/statestore/space.ts"
|
||||||
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
|
import UnreadCount from "./UnreadCount.tsx"
|
||||||
import HomeIcon from "@/icons/home.svg?react"
|
import HomeIcon from "@/icons/home.svg?react"
|
||||||
import NotificationsIcon from "@/icons/notifications.svg?react"
|
import NotificationsIcon from "@/icons/notifications.svg?react"
|
||||||
import PersonIcon from "@/icons/person.svg?react"
|
import PersonIcon from "@/icons/person.svg?react"
|
||||||
|
@ -22,7 +24,7 @@ import TagIcon from "@/icons/tag.svg?react"
|
||||||
import "./RoomList.css"
|
import "./RoomList.css"
|
||||||
|
|
||||||
export interface FakeSpaceProps {
|
export interface FakeSpaceProps {
|
||||||
space: RoomListFilter | null
|
space: Space | null
|
||||||
setSpace: (space: RoomListFilter | null) => void
|
setSpace: (space: RoomListFilter | null) => void
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
|
@ -43,10 +45,11 @@ const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FakeSpace = ({ space, setSpace, isActive }: FakeSpaceProps) => {
|
const FakeSpace = ({ space, setSpace, isActive }: FakeSpaceProps) => {
|
||||||
|
const unreads = useEventAsState(space?.counts)
|
||||||
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={() => setSpace(space)}>
|
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={() => setSpace(space)}>
|
||||||
|
<UnreadCount counts={unreads} space={true} />
|
||||||
{getFakeSpaceIcon(space)}
|
{getFakeSpaceIcon(space)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FakeSpace
|
export default FakeSpace
|
||||||
|
|
|
@ -51,6 +51,17 @@ div.space-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> div.room-entry-unreads {
|
||||||
|
z-index: 2;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> div.unread-count {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,8 +149,9 @@ div.room-entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> div.room-entry-unreads {
|
div.room-entry-unreads {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -167,7 +179,7 @@ div.room-entry {
|
||||||
padding-inline: var(--unread-count-padding-inline);
|
padding-inline: var(--unread-count-padding-inline);
|
||||||
padding-block: var(--unread-count-padding-block);
|
padding-block: var(--unread-count-padding-block);
|
||||||
|
|
||||||
&.notified, &.marked-unread, &.highlighted {
|
&.big {
|
||||||
--unread-count-size: 1.5rem;
|
--unread-count-size: 1.5rem;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -184,6 +196,18 @@ div.room-entry {
|
||||||
&.highlighted {
|
&.highlighted {
|
||||||
background-color: var(--unread-counter-highlight-bg);
|
background-color: var(--unread-counter-highlight-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.space {
|
||||||
|
--unread-count-size: .75rem;
|
||||||
|
background-color: var(--space-unread-counter-message-bg);
|
||||||
|
|
||||||
|
&.notified {
|
||||||
|
background-color: var(--space-unread-counter-notification-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlighted {
|
||||||
|
background-color: var(--space-unread-counter-highlight-bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +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 React, { use, useCallback, useRef, useState } from "react"
|
import React, { use, useCallback, useRef, useState } from "react"
|
||||||
import { DirectChatSpace, RoomListFilter, UnreadsSpace } from "@/api/statestore/space.ts"
|
import { RoomListFilter } from "@/api/statestore/space.ts"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import reverseMap from "@/util/reversemap.ts"
|
import reverseMap from "@/util/reversemap.ts"
|
||||||
|
@ -75,12 +75,6 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomListFilter = client.store.roomListFilterFunc
|
const roomListFilter = client.store.roomListFilterFunc
|
||||||
const pseudoSpaces = [
|
|
||||||
null,
|
|
||||||
DirectChatSpace,
|
|
||||||
UnreadsSpace,
|
|
||||||
client.store.spaceOrphans,
|
|
||||||
]
|
|
||||||
return <div className="room-list-wrapper">
|
return <div className="room-list-wrapper">
|
||||||
<div className="room-search-wrapper">
|
<div className="room-search-wrapper">
|
||||||
<input
|
<input
|
||||||
|
@ -98,11 +92,12 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-bar">
|
<div className="space-bar">
|
||||||
{pseudoSpaces.map(pseudoSpace => <FakeSpace
|
<FakeSpace space={null} setSpace={setSpace} isActive={space === null} />
|
||||||
key={pseudoSpace?.id ?? "null"}
|
{client.store.pseudoSpaces.map(pseudoSpace => <FakeSpace
|
||||||
|
key={pseudoSpace.id}
|
||||||
space={pseudoSpace}
|
space={pseudoSpace}
|
||||||
setSpace={setSpace}
|
setSpace={setSpace}
|
||||||
isActive={space?.id === pseudoSpace?.id}
|
isActive={space?.id === pseudoSpace.id}
|
||||||
/>)}
|
/>)}
|
||||||
{spaces.map(roomID => <Space
|
{spaces.map(roomID => <Space
|
||||||
key={roomID}
|
key={roomID}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import Client from "@/api/client.ts"
|
||||||
import { getRoomAvatarURL } from "@/api/media.ts"
|
import { getRoomAvatarURL } from "@/api/media.ts"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
|
import UnreadCount from "./UnreadCount.tsx"
|
||||||
import "./RoomList.css"
|
import "./RoomList.css"
|
||||||
|
|
||||||
export interface SpaceProps {
|
export interface SpaceProps {
|
||||||
|
@ -28,11 +29,13 @@ export interface SpaceProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Space = ({ roomID, client, onClick, isActive }: SpaceProps) => {
|
const Space = ({ roomID, client, onClick, isActive }: SpaceProps) => {
|
||||||
|
const unreads = useEventAsState(client.store.spaceEdges.get(roomID)?.counts)
|
||||||
const room = useEventAsState(client.store.rooms.get(roomID)?.meta)
|
const room = useEventAsState(client.store.rooms.get(roomID)?.meta)
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={onClick} data-target-space={roomID}>
|
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={onClick} data-target-space={roomID}>
|
||||||
|
<UnreadCount counts={unreads} space={true} />
|
||||||
<img src={getRoomAvatarURL(room)} alt={room.name} title={room.name} className="avatar" />
|
<img src={getRoomAvatarURL(room)} alt={room.name} title={room.name} className="avatar" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
73
web/src/ui/roomlist/UnreadCount.tsx
Normal file
73
web/src/ui/roomlist/UnreadCount.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// 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 { SpaceUnreadCounts } from "@/api/statestore/space.ts"
|
||||||
|
|
||||||
|
interface UnreadCounts extends SpaceUnreadCounts {
|
||||||
|
marked_unread?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnreadCountProps {
|
||||||
|
counts: UnreadCounts | null
|
||||||
|
space?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnreadCount = ({ counts, space }: UnreadCountProps) => {
|
||||||
|
if (!counts) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const unreadCount = space
|
||||||
|
? counts.unread_highlights || counts.unread_notifications || counts.unread_messages
|
||||||
|
: counts.unread_messages || counts.unread_notifications || counts.unread_highlights
|
||||||
|
if (!unreadCount && !counts.marked_unread) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const countIsBig = !space && Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread)
|
||||||
|
let unreadCountDisplay = unreadCount.toString()
|
||||||
|
if (unreadCount > 999 && countIsBig) {
|
||||||
|
unreadCountDisplay = "99+"
|
||||||
|
} else if (unreadCount > 9999 && countIsBig) {
|
||||||
|
unreadCountDisplay = "999+"
|
||||||
|
}
|
||||||
|
const classNames = ["unread-count"]
|
||||||
|
if (countIsBig) {
|
||||||
|
classNames.push("big")
|
||||||
|
}
|
||||||
|
let unreadCountTitle = unreadCount.toString()
|
||||||
|
if (space) {
|
||||||
|
classNames.push("space")
|
||||||
|
unreadCountTitle = [
|
||||||
|
counts.unread_highlights && `${counts.unread_highlights} highlights`,
|
||||||
|
counts.unread_notifications && `${counts.unread_notifications} notifications`,
|
||||||
|
counts.unread_messages && `${counts.unread_messages} messages`,
|
||||||
|
].filter(x => !!x).join("\n")
|
||||||
|
}
|
||||||
|
if (counts.marked_unread) {
|
||||||
|
classNames.push("marked-unread")
|
||||||
|
}
|
||||||
|
if (counts.unread_notifications) {
|
||||||
|
classNames.push("notified")
|
||||||
|
}
|
||||||
|
if (counts.unread_highlights) {
|
||||||
|
classNames.push("highlighted")
|
||||||
|
}
|
||||||
|
return <div className="room-entry-unreads">
|
||||||
|
<div title={unreadCountTitle} className={classNames.join(" ")}>
|
||||||
|
{unreadCountDisplay}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnreadCount
|
Loading…
Add table
Reference in a new issue