Moderate Kick from the member list, and read a restriction in words
The menu asks Kick what it may do the way it asks IRC: from what is already on screen. A moderator's own row says they are one, so that is the test - which means a moderator who has not spoken yet is offered nothing, and that is the honest reading rather than offering actions that come back refused. Actions are named for what they do on the service. "Kick" is a timeout on Kick, because there is nobody to remove from a livestream chat. The mode chip now passes words through instead of reading them a letter at a time - "followers only" is not f, o, l, l, o, w. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: f78d18c85c...0a3135a11a
@@ -7,13 +7,19 @@ import type { BufferEntry } from '../state/store'
|
||||
import type { Message } from '../../../shared/wire'
|
||||
|
||||
/**
|
||||
* What a channel's mode letters mean, for the ones worth explaining.
|
||||
* What a channel's restrictions mean, for the ones worth explaining.
|
||||
*
|
||||
* Not a complete table and not trying to be: every network invents its own,
|
||||
* and the ones below are the handful that change whether you can say anything.
|
||||
* An unknown letter is shown as itself rather than guessed at.
|
||||
* IRC writes them as letters because that is what its server says; Kick has
|
||||
* no such spelling and words them instead, so anything already in words is
|
||||
* passed through rather than read a letter at a time - "followers only" is
|
||||
* not f, o, l, l, o, w.
|
||||
*
|
||||
* The letter table is not complete and is not trying to be: every network
|
||||
* invents its own, and these are the handful that change whether you can say
|
||||
* anything. An unknown letter is shown as itself.
|
||||
*/
|
||||
function describeModes(modes: string): string {
|
||||
if (/[\s]/.test(modes) || !/^\+?[a-z]+$/i.test(modes)) return modes
|
||||
const meanings: Record<string, string> = {
|
||||
m: 'moderated — only voiced users may speak',
|
||||
i: 'invite only',
|
||||
|
||||
@@ -47,6 +47,21 @@ function shortBadge(prefix: string): string {
|
||||
return short[prefix] ?? prefix.slice(0, 3).toUpperCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* What the local user may do in a Kick channel.
|
||||
*
|
||||
* Read off their own badge in the list, which is only there once they have
|
||||
* spoken - so a moderator who has said nothing yet is offered nothing. That
|
||||
* is the honest reading: nothing here knows they are a moderator until Kick
|
||||
* says so, and the alternative is offering actions that come back refused.
|
||||
*/
|
||||
function kickPermissions(members: Member[], me: string): Record<string, boolean> {
|
||||
if (!me) return {}
|
||||
const mine = members.find((m) => m.nick.toLowerCase() === me.toLowerCase())?.prefix ?? ''
|
||||
const moderator = mine === 'moderator' || mine === 'broadcaster'
|
||||
return { canKick: moderator, canBan: moderator, canMute: moderator }
|
||||
}
|
||||
|
||||
/**
|
||||
* Why the list says what it says, for the one service where it needs saying.
|
||||
*/
|
||||
@@ -102,8 +117,16 @@ export function NickList(): JSX.Element {
|
||||
// prefix, including ours, so what we may do is whatever our own prefix says.
|
||||
// Both are advisory - the server re-checks either way and refuses if we were
|
||||
// wrong, exactly as any other client would be refused.
|
||||
// Kick answers this from the badges it already sent: the local user is a
|
||||
// moderator of a channel if their own row says so. Same shape as IRC's,
|
||||
// and advisory in the same way - Kick re-checks and refuses if we were
|
||||
// wrong, which it explains far better than a status code would.
|
||||
const perms =
|
||||
account?.service === 'irc' ? ircPermissions(members, account.currentNick) : (buffer && permissions[buffer.id]) || {}
|
||||
account?.service === 'irc'
|
||||
? ircPermissions(members, account.currentNick)
|
||||
: account?.service === 'kick'
|
||||
? kickPermissions(members, account.currentNick || account.displayName)
|
||||
: (buffer && permissions[buffer.id]) || {}
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const filtered = query
|
||||
@@ -192,6 +215,7 @@ export function NickList(): JSX.Element {
|
||||
canSendFile={account?.service === 'irc'}
|
||||
canCall={canCall && !!member.userId}
|
||||
perms={perms}
|
||||
service={account?.service}
|
||||
onToggleBlock={toggleBlocked}
|
||||
onMention={() => store.startReply('', member.nick, '')}
|
||||
onModerate={(action) => {
|
||||
@@ -211,6 +235,22 @@ export function NickList(): JSX.Element {
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
return
|
||||
}
|
||||
if (account.service === 'kick') {
|
||||
// "Kick" is a timeout on Kick - there is nothing to
|
||||
// remove somebody from - and "mute" means the same thing,
|
||||
// so both become the short ban that they are.
|
||||
const kickAction = action === 'ban' ? 'ban' : 'timeout'
|
||||
void window.moho
|
||||
.rpc('moderateKickUser', {
|
||||
accountId: account.id,
|
||||
bufferId: buffer.id,
|
||||
username: member.nick,
|
||||
action: kickAction,
|
||||
minutes: 10
|
||||
})
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
return
|
||||
}
|
||||
if (action === 'op' || action === 'deop' || action === 'voice') {
|
||||
store.toast('error', 'Only IRC has channel ranks')
|
||||
return
|
||||
@@ -273,6 +313,8 @@ interface MemberRowProps {
|
||||
/** IRC carries a file directly between two people; nothing else here does. */
|
||||
canSendFile: boolean
|
||||
perms: { canKick?: boolean; canBan?: boolean; canMute?: boolean; canOp?: boolean }
|
||||
/** Which service this row belongs to, so an action is named as it acts. */
|
||||
service: string | undefined
|
||||
onToggleBlock: (key: string) => void
|
||||
onMention: () => void
|
||||
onModerate: (action: ModerationAction) => void
|
||||
@@ -292,6 +334,7 @@ function MemberRow({
|
||||
canWhisper,
|
||||
canSendFile,
|
||||
perms,
|
||||
service,
|
||||
onToggleBlock,
|
||||
onMention,
|
||||
onModerate,
|
||||
@@ -321,7 +364,16 @@ function MemberRow({
|
||||
? ([{ label: 'Mute', icon: 'volume_off', onClick: () => onModerate('mute') }] as MenuEntry[])
|
||||
: []),
|
||||
...(perms.canKick
|
||||
? ([{ label: 'Kick', icon: 'logout', danger: true, onClick: () => onModerate('kick') }] as MenuEntry[])
|
||||
? ([
|
||||
{
|
||||
// Named for what it does on this service. There is nobody to
|
||||
// remove from a livestream chat, so on Kick it is a timeout.
|
||||
label: service === 'kick' ? 'Time out for 10 minutes' : 'Kick',
|
||||
icon: 'logout',
|
||||
danger: true,
|
||||
onClick: () => onModerate('kick')
|
||||
}
|
||||
] as MenuEntry[])
|
||||
: []),
|
||||
...(perms.canBan
|
||||
? ([{ label: 'Ban', icon: 'gavel', danger: true, onClick: () => onModerate('ban') }] as MenuEntry[])
|
||||
|
||||
Reference in New Issue
Block a user