Merge branch 'development'
This commit is contained in:
commit
f9e4fbf1c4
8 files changed
+114
-37
No files matched your search
@@ -33,6 +33,8 @@ Each section has two parts:
|
||||
- Fixed the window being pulled back to the open conversation's server whenever you chose another: the daemon link coming up is acted on once, a refresh no longer follows the open conversation, and the tray is told only what changed ([moho `745641a`](https://github.com/Moho-Chat/moho/commit/745641a))
|
||||
- Matrix's invisible is called Offline in the plaque, its popout and the tray, which is what it sets ([moho `745641a`](https://github.com/Moho-Chat/moho/commit/745641a))
|
||||
- A room muted on a Matrix account can now be unmuted from the menu, including a mute set from Element ([moho `745641a`](https://github.com/Moho-Chat/moho/commit/745641a), [nobilis `59db82c`](https://github.com/Moho-Chat/nobilis/commit/59db82c))
|
||||
- Server rail tiles now count the mentions waiting in them rather than the messages, and a muted room counts for nothing, mentions included ([moho `0dc048c`](https://github.com/Moho-Chat/moho/commit/0dc048c))
|
||||
- Fixed a room unmuted on a Matrix account still being drawn as muted; a mute or unmute the server did not take is now reported with its reason, and Matrix requests to a server that does not answer give up ([moho `95f1784`](https://github.com/Moho-Chat/moho/commit/95f1784), [nobilis `d1bbcaf`](https://github.com/Moho-Chat/nobilis/commit/d1bbcaf))
|
||||
- Discord requests now give up on a connection that has gone quiet, a history catch-up always releases its place, and the gateway logs what it receives and why a message is dropped ([moho `b151ef8`](https://github.com/Moho-Chat/moho/commit/b151ef8), [nobilis `a34821e`](https://github.com/Moho-Chat/nobilis/commit/a34821e))
|
||||
- Fixed Discord reconnecting every few seconds after being told it is rate limited: it now waits out the minute Discord asks for ([nobilis `59db82c`](https://github.com/Moho-Chat/nobilis/commit/59db82c))
|
||||
|
||||
|
||||
+1
-1
Submodule nobilis updated: f392d70c8d...58dfa5894d.
@@ -94,14 +94,14 @@ const DISCORD = `(() => {
|
||||
const ch = (id, name, category, extra = {}) => ({
|
||||
id: acc + '|' + id, accountId: acc, kind: 'channel', name: 'Button Eye Crochet/#' + name,
|
||||
lastActivityTs: now, groupId: g1, category, position: Number(id.replace(/\\D/g, '')) || 0,
|
||||
unread: 0, highlight: false, ...extra
|
||||
unread: 0, highlight: false, mentions: 0, ...extra
|
||||
})
|
||||
const buffers = [
|
||||
ch('c1', '📜-rules', undefined),
|
||||
ch('c2', '📢-announcements', undefined, { unread: 2 }),
|
||||
ch('c3', '🤠-general', 'TEXT CHANNELS'),
|
||||
ch('c4', '📹-clips', 'TEXT CHANNELS', { unread: 5 }),
|
||||
ch('c5', '📷-screenshots', 'TEXT CHANNELS', { unread: 1, highlight: true }),
|
||||
ch('c5', '📷-screenshots', 'TEXT CHANNELS', { unread: 1, highlight: true, mentions: 1 }),
|
||||
ch('c6', '💩-shitpost', 'TEXT CHANNELS', { unread: 9 }),
|
||||
ch('c7', '🎨-crafts', 'TEXT CHANNELS'),
|
||||
ch('c8', '🐈-cats', 'ANIMALS', { unread: 3 }),
|
||||
@@ -136,7 +136,7 @@ const DISCORD = `(() => {
|
||||
{ id: g2, accountId: acc, service: 'discord', kind: 'guild', name: 'Moho Dev', position: 1 }
|
||||
],
|
||||
buffers: [...s.buffers.filter((b) => b.accountId !== acc), ...buffers,
|
||||
{ id: acc + '|d1', accountId: acc, kind: 'channel', name: 'Moho Dev/#general', lastActivityTs: now, groupId: g2, unread: 2, highlight: true }],
|
||||
{ id: acc + '|d1', accountId: acc, kind: 'channel', name: 'Moho Dev/#general', lastActivityTs: now, groupId: g2, unread: 33, highlight: true, mentions: 2 }],
|
||||
messagesByBuffer: { ...s.messagesByBuffer, [general]: messages },
|
||||
presenceByBuffer: { ...s.presenceByBuffer, [general]: members([['Gaunt King', 'online'], ['Clarence', 'idle'], ['Wren', 'dnd'], ['Salastil', 'online'], ['Old Friend', 'offline'], ['Quiet One', 'offline']]) },
|
||||
activeGroupId: g1,
|
||||
|
||||
@@ -60,6 +60,8 @@ interface TileProps {
|
||||
active: boolean
|
||||
unread: number
|
||||
highlight: boolean
|
||||
/** How many unread messages mention this account: the number the tile shows. */
|
||||
mentions: number
|
||||
draggable: boolean
|
||||
dropTarget: boolean
|
||||
/** Being dragged right now: it leaves a gap where it was. */
|
||||
@@ -179,7 +181,7 @@ function isMerge(e: React.DragEvent): boolean {
|
||||
}
|
||||
|
||||
function RailTile(props: TileProps): JSX.Element {
|
||||
const { group, active, unread, highlight, draggable, dropTarget, customIcon, muted, lifted, mergeTarget } = props
|
||||
const { group, active, unread, highlight, mentions, draggable, dropTarget, customIcon, muted, lifted, mergeTarget } = props
|
||||
const { menu, open, close } = useContextMenu()
|
||||
const store = useStore()
|
||||
// What is waiting inside this server, for "Mark as read".
|
||||
@@ -299,15 +301,17 @@ function RailTile(props: TileProps): JSX.Element {
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
{/* A mention waiting in here says how many are waiting, in the corner
|
||||
the service mark sits in - the mark gives way, since a count is what
|
||||
somebody scanning this column came to see. */}
|
||||
{highlight && unread > 0 && (
|
||||
<span className="rail-count highlight" title={`${unread} unread, with a mention`}>
|
||||
{unread > 99 ? '99+' : unread}
|
||||
{/* Mentions say how many are waiting, in the corner the service mark sits
|
||||
in - the mark gives way, since a count is what somebody scanning this
|
||||
column came to see. Only mentions: how many messages there are in a
|
||||
busy channel is not what this column is for, and the pill beside the
|
||||
tile already says there is something to read. */}
|
||||
{mentions > 0 && (
|
||||
<span className="rail-count highlight" title={`${mentions} mention${mentions === 1 ? '' : 's'}`}>
|
||||
{mentions > 99 ? '99+' : mentions}
|
||||
</span>
|
||||
)}
|
||||
{badge && !(highlight && unread > 0) && (
|
||||
{badge && mentions === 0 && (
|
||||
<span className={`rail-service${highlight ? ' highlight' : ''}`}>
|
||||
{badge.colour && badge.mark ? (
|
||||
<img src={badge.mark} alt="" draggable={false} />
|
||||
@@ -419,11 +423,14 @@ export function ServerRail(): JSX.Element | null {
|
||||
|
||||
// Unread rolls up from the buffers under each entry, so a guild whose
|
||||
// channels are all collapsed away still shows it has something waiting.
|
||||
const totals = new Map<string, { unread: number; highlight: boolean }>()
|
||||
const bump = (key: string, b: BufferEntry): void => {
|
||||
const t = totals.get(key) || { unread: 0, highlight: false }
|
||||
const totals = new Map<string, { unread: number; highlight: boolean; mentions: number }>()
|
||||
// `direct` is a page of conversations addressed to you, where every unread
|
||||
// message is one that is for you and so counts as a mention.
|
||||
const bump = (key: string, b: BufferEntry, direct = false): void => {
|
||||
const t = totals.get(key) || { unread: 0, highlight: false, mentions: 0 }
|
||||
t.unread += b.unread
|
||||
t.highlight = t.highlight || b.highlight
|
||||
t.mentions += direct ? b.unread : b.mentions
|
||||
totals.set(key, t)
|
||||
}
|
||||
const all = buffers as BufferEntry[]
|
||||
@@ -434,7 +441,7 @@ export function ServerRail(): JSX.Element | null {
|
||||
// A buffer can count on more than one tile: where it lives, and on any
|
||||
// aggregate page that also lists it. Both are places the user would look.
|
||||
if (b.groupId) bump(b.groupId, b)
|
||||
if (isDirectMessage(b)) bump(DM_GROUP_ID, b)
|
||||
if (isDirectMessage(b)) bump(DM_GROUP_ID, b, true)
|
||||
if (pinned.includes(b.id)) bump(PINNED_GROUP_ID, b)
|
||||
}
|
||||
|
||||
@@ -552,6 +559,7 @@ export function ServerRail(): JSX.Element | null {
|
||||
onBrowse={g.kind === 'space' ? () => setBrowsing(g) : undefined}
|
||||
unread={t?.unread ?? 0}
|
||||
highlight={t?.highlight ?? false}
|
||||
mentions={t?.mentions ?? 0}
|
||||
// Direct messages and pinned lead the rail by definition, so
|
||||
// there is nowhere for them to be dragged to.
|
||||
draggable={!isFixedEntry(g)}
|
||||
@@ -599,6 +607,7 @@ export function ServerRail(): JSX.Element | null {
|
||||
open={open}
|
||||
unread={entry.members.reduce((n, m) => n + (totals.get(m.id)?.unread ?? 0), 0)}
|
||||
highlight={entry.members.some((m) => totals.get(m.id)?.highlight)}
|
||||
mentions={entry.members.reduce((n, m) => n + (totals.get(m.id)?.mentions ?? 0), 0)}
|
||||
onToggle={() => toggleFolder(entry.id)}
|
||||
lifted={dragging === `${FOLDER_DRAG_PREFIX}${entry.id}`}
|
||||
onDragStart={() => beginDrag(`${FOLDER_DRAG_PREFIX}${entry.id}`)}
|
||||
@@ -788,6 +797,7 @@ function FolderTile(props: {
|
||||
/** What is waiting in the servers inside, so a closed folder can say so. */
|
||||
unread: number
|
||||
highlight: boolean
|
||||
mentions: number
|
||||
onToggle: () => void
|
||||
onContextMenu: (e: React.MouseEvent) => void
|
||||
onDragStart: () => void
|
||||
@@ -795,7 +805,7 @@ function FolderTile(props: {
|
||||
onDrop: () => void
|
||||
onDragEnd: () => void
|
||||
}): JSX.Element {
|
||||
const { folder, members, customIcons, dropTarget, lifted, open, unread, highlight } = props
|
||||
const { folder, members, customIcons, dropTarget, lifted, open, unread, mentions } = props
|
||||
// Open, its servers show their own; closed, what is inside would be
|
||||
// invisible without this.
|
||||
const waiting = !open && unread > 0
|
||||
@@ -862,14 +872,14 @@ function FolderTile(props: {
|
||||
{/* Once the face is a grid of other people's icons, nothing says it is
|
||||
a folder any more - so it takes the same corner badge a guild uses
|
||||
to name its service. An empty folder is already unmistakably one. */}
|
||||
{members.length > 0 && !(waiting && highlight) && (
|
||||
{members.length > 0 && !(!open && mentions > 0) && (
|
||||
<span className="rail-service">
|
||||
<Icon name="folder" size={11} />
|
||||
</span>
|
||||
)}
|
||||
{waiting && highlight && (
|
||||
<span className="rail-count highlight" title={`${unread} unread, with a mention`}>
|
||||
{unread > 99 ? '99+' : unread}
|
||||
{!open && mentions > 0 && (
|
||||
<span className="rail-count highlight" title={`${mentions} mention${mentions === 1 ? '' : 's'}`}>
|
||||
{mentions > 99 ? '99+' : mentions}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -81,16 +81,22 @@ describe('folders', () => {
|
||||
|
||||
describe('what a muted place still says', () => {
|
||||
const buf = (extra: Partial<BufferEntry>): BufferEntry =>
|
||||
({ id: 'a|#x', accountId: 'a', kind: 'channel', name: '#x', lastActivityTs: 0, groupId: 'g', unread: 3, highlight: false, ...extra }) as BufferEntry
|
||||
({ id: 'a|#x', accountId: 'a', kind: 'channel', name: '#x', lastActivityTs: 0, groupId: 'g', unread: 3, highlight: false, mentions: 0, ...extra }) as BufferEntry
|
||||
|
||||
it('counts a quiet muted channel for nothing', () => {
|
||||
it('counts a muted channel for nothing', () => {
|
||||
expect(countsTowardRail(buf({}), ['a|#x'], [])).toBe(false)
|
||||
expect(countsTowardRail(buf({}), [], [], ['g'])).toBe(false)
|
||||
})
|
||||
|
||||
it('still counts it once somebody has used your name', () => {
|
||||
expect(countsTowardRail(buf({ highlight: true }), ['a|#x'], [])).toBe(true)
|
||||
expect(countsTowardRail(buf({ highlight: true }), [], [], ['g'])).toBe(true)
|
||||
it('counts it for nothing even once somebody has used your name', () => {
|
||||
expect(countsTowardRail(buf({ highlight: true, mentions: 2 }), ['a|#x'], [])).toBe(false)
|
||||
expect(countsTowardRail(buf({ highlight: true, mentions: 2 }), [], [], ['g'])).toBe(false)
|
||||
// A mute made on the account itself is the same.
|
||||
expect(countsTowardRail(buf({ highlight: true, mentions: 2, serverMuted: true }), [], [])).toBe(false)
|
||||
})
|
||||
|
||||
it('counts what is not muted', () => {
|
||||
expect(countsTowardRail(buf({ highlight: true, mentions: 2 }), [], [])).toBe(true)
|
||||
})
|
||||
|
||||
it('never counts what has no row to click through to', () => {
|
||||
|
||||
@@ -263,9 +263,9 @@ export function isMutedBuffer(
|
||||
/**
|
||||
* The buffers whose unread should reach a rail tile: what the pane below it
|
||||
* would actually list. A hidden buffer has no row to click through to, and a
|
||||
* muted one deliberately does not ask for attention - except to say that
|
||||
* somebody used its owner's name, which muting a place has never meant not
|
||||
* wanting to hear (the clients this one is measured against all keep that).
|
||||
* muted one is muted - it does not count, mentions included. Muting a place is
|
||||
* the one way to say "not now", and a tile that goes on counting what was
|
||||
* muted says the setting does nothing.
|
||||
*/
|
||||
export function countsTowardRail(
|
||||
buffer: BufferEntry,
|
||||
@@ -273,7 +273,7 @@ export function countsTowardRail(
|
||||
hidden: string[],
|
||||
mutedGroups: string[] = []
|
||||
): boolean {
|
||||
return !hidden.includes(buffer.id) && (buffer.highlight || !isMutedBuffer(buffer, muted, mutedGroups))
|
||||
return !hidden.includes(buffer.id) && !isMutedBuffer(buffer, muted, mutedGroups)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ChatStore } from './store'
|
||||
|
||||
/** The store, with a conversation already in its list, and the daemon's update for it applied. */
|
||||
function after(update: Record<string, unknown>, before: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
const store = new ChatStore() as unknown as {
|
||||
state: { buffers: unknown[] }
|
||||
handleBufferListChange: (data: unknown) => void
|
||||
}
|
||||
const base = { id: 'm|!r', accountId: 'm', kind: 'channel', name: '!r', lastActivityTs: 0, unread: 0, highlight: false, mentions: 0 }
|
||||
store.state = { ...store.state, buffers: [{ ...base, ...before }] }
|
||||
// What the daemon sends carries no unread tally: that is this window's own.
|
||||
store.handleBufferListChange({ id: base.id, accountId: base.accountId, kind: base.kind, name: base.name, lastActivityTs: 0, ...update })
|
||||
return store.state.buffers[0] as Record<string, unknown>
|
||||
}
|
||||
|
||||
describe('an update to a conversation the window already has', () => {
|
||||
it('takes a mute off when the daemon leaves it out', () => {
|
||||
// The daemon omits a flag that is off, so absent has to mean cleared: a
|
||||
// room unmuted on the account stayed drawn as muted, with an Unmute that
|
||||
// had nothing left to do.
|
||||
expect(after({}, { serverMuted: true }).serverMuted).toBe(false)
|
||||
expect(after({ serverMuted: true }, {}).serverMuted).toBe(true)
|
||||
})
|
||||
|
||||
it('clears the other flags the same way, and keeps the unread count it holds', () => {
|
||||
const b = after({}, { favourite: true, lowPriority: true, readOnly: 'No permission', forum: true, unread: 4, mentions: 2, highlight: true })
|
||||
expect([b.favourite, b.lowPriority, b.readOnly, b.forum]).toEqual([false, false, undefined, false])
|
||||
expect([b.unread, b.mentions, b.highlight]).toEqual([4, 2, true])
|
||||
})
|
||||
})
|
||||
@@ -72,6 +72,12 @@ export interface ForumPageCache {
|
||||
export interface BufferEntry extends WireBuffer {
|
||||
unread: number
|
||||
highlight: boolean
|
||||
/**
|
||||
* How many of the unread messages mention this account. The count the rail
|
||||
* shows: a tile that says 33 for thirty-three messages in a busy channel is
|
||||
* noise, where one that says 2 for two people asking for you is the point.
|
||||
*/
|
||||
mentions: number
|
||||
/**
|
||||
* Left unread on purpose, rather than merely not read yet.
|
||||
*
|
||||
@@ -2191,6 +2197,7 @@ export class ChatStore {
|
||||
const buffers = wire.map((b) => ({
|
||||
unread: existing.get(b.id)?.unread ?? 0,
|
||||
highlight: existing.get(b.id)?.highlight ?? false,
|
||||
mentions: existing.get(b.id)?.mentions ?? 0,
|
||||
...b
|
||||
}))
|
||||
this.set({ buffers })
|
||||
@@ -3238,7 +3245,7 @@ export class ChatStore {
|
||||
// Reading a room is how a deliberate mark is taken off, so the
|
||||
// flag clears with the count. Anything else would need a second
|
||||
// gesture to undo the first, and nobody would find it.
|
||||
b.id === data.bufferId ? { ...b, unread: 0, highlight: false, markedUnread: false } : b
|
||||
b.id === data.bufferId ? { ...b, unread: 0, highlight: false, mentions: 0, markedUnread: false } : b
|
||||
)
|
||||
})
|
||||
break
|
||||
@@ -3650,7 +3657,8 @@ export class ChatStore {
|
||||
remoteId: roomIdOrAlias,
|
||||
syncing: true,
|
||||
unread: 0,
|
||||
highlight: false
|
||||
highlight: false,
|
||||
mentions: 0
|
||||
}
|
||||
this.set({ buffers: [...this.state.buffers, stand_in] })
|
||||
try {
|
||||
@@ -3723,13 +3731,33 @@ export class ChatStore {
|
||||
// leaves an optional field out when it is not set, so a plain spread
|
||||
// kept the last value it ever had - a room that reconnected stayed
|
||||
// marked as interrupted, and the banner over it stayed up.
|
||||
//
|
||||
// The same for every flag the daemon leaves out when it is off - muted on
|
||||
// the account, favourite, low priority, read-only, a forum. A merge that
|
||||
// kept the last value it ever had left a room unmuted on the server still
|
||||
// drawn as muted here, with an Unmute that had nothing left to do.
|
||||
this.set({
|
||||
buffers: buffers.map((b) => (b.id === data.id ? { ...b, ...data, link: data.link, syncing: data.syncing, topic: data.topic } : b))
|
||||
buffers: buffers.map((b) =>
|
||||
b.id === data.id
|
||||
? {
|
||||
...b,
|
||||
...data,
|
||||
link: data.link,
|
||||
syncing: data.syncing,
|
||||
topic: data.topic,
|
||||
serverMuted: data.serverMuted ?? false,
|
||||
favourite: data.favourite ?? false,
|
||||
lowPriority: data.lowPriority ?? false,
|
||||
readOnly: data.readOnly,
|
||||
forum: data.forum ?? false
|
||||
}
|
||||
: b
|
||||
)
|
||||
})
|
||||
this.followPeekJoin(data)
|
||||
return
|
||||
}
|
||||
this.set({ buffers: [...buffers, { unread: 0, highlight: false, ...data }] })
|
||||
this.set({ buffers: [...buffers, { unread: 0, highlight: false, mentions: 0, ...data }] })
|
||||
// Filed under a rail entry this window has never been told of. An
|
||||
// account's own entry is not announced when the account is made - the
|
||||
// daemon lists one for every account when asked - so the first
|
||||
@@ -3878,7 +3906,7 @@ export class ChatStore {
|
||||
this.set({
|
||||
buffers: this.state.buffers.map((b) =>
|
||||
b.id === bufferId
|
||||
? { ...b, unread: b.unread + 1, highlight: b.highlight || !!msg.isHighlight }
|
||||
? { ...b, unread: b.unread + 1, highlight: b.highlight || !!msg.isHighlight, mentions: b.mentions + (msg.isHighlight ? 1 : 0) }
|
||||
: b
|
||||
)
|
||||
})
|
||||
@@ -3943,7 +3971,7 @@ export class ChatStore {
|
||||
replyingTo: null,
|
||||
editingId: '',
|
||||
buffers: this.state.buffers.map((b) =>
|
||||
b.id === bufferId ? { ...b, unread: 0, highlight: false } : b
|
||||
b.id === bufferId ? { ...b, unread: 0, highlight: false, mentions: 0 } : b
|
||||
)
|
||||
}
|
||||
if (dividerTs === undefined && buffer.unread > 0) {
|
||||
@@ -4129,7 +4157,7 @@ export class ChatStore {
|
||||
// here too - the stored timestamp only decides what counts next time.
|
||||
this.set({
|
||||
buffers: this.state.buffers.map((b) =>
|
||||
bufferIds.includes(b.id) ? { ...b, unread: 0, highlight: false } : b
|
||||
bufferIds.includes(b.id) ? { ...b, unread: 0, highlight: false, mentions: 0 } : b
|
||||
)
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
Reference in new issue
Block a user