Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b81810f5cc | ||
|
|
44208364cd | ||
|
|
335ac8b59d |
@@ -0,0 +1,23 @@
|
||||
# On a merge, .gitmodules keeps the answer this branch already has.
|
||||
#
|
||||
# master names the daemon at github.com because master is what a stranger
|
||||
# clones, and development names it at the Gitea instance where the work
|
||||
# happens. Without this, a merge that touched .gitmodules on both sides would
|
||||
# stop and ask which host is right - and the answer is always "the one the
|
||||
# branch being merged into already said".
|
||||
#
|
||||
# `merge=ours` needs a driver by that name, which is one line per clone and
|
||||
# not something a repository can carry for you:
|
||||
#
|
||||
# git config merge.ours.driver true
|
||||
#
|
||||
# Without it git falls back to an ordinary merge, which conflicts rather than
|
||||
# doing the wrong thing quietly - so a clone that has not run it is no worse
|
||||
# off than before this file existed.
|
||||
#
|
||||
# The edge, because it is a real one: this keeps the *whole file*, not just the
|
||||
# URL. If development ever adds a second submodule, master takes none of that
|
||||
# entry while still taking the gitlink the merge brings with it - a submodule
|
||||
# with nothing in .gitmodules to say where it comes from. Adding a submodule is
|
||||
# therefore a thing to re-do by hand on master, and the only such thing.
|
||||
/.gitmodules merge=ours
|
||||
@@ -228,17 +228,6 @@ called `subscribe` for that buffer.
|
||||
the stream plays in the window as the person (their Turbo or subscriptions count) with Twitch's ad
|
||||
breaks said and never skipped.
|
||||
|
||||
- **Telegram** - MTProto through grammers (a pure-Rust client, pinned), with each account carrying its own
|
||||
`api_id` and `api_hash` from my.telegram.org (moho embeds none, so no install shares a key) and one saved
|
||||
session per account in a private file that is never re-created on a hiccup. Sign-in by phone, code and cloud
|
||||
password. Chats are read from the dialog list and become buffers under Direct Messages, Groups and Channels
|
||||
(a channel the account cannot write in shows the lock message instead of the message box); history, live
|
||||
messages, edits and deletions, formatting both ways, replies and forwards, service messages as cards, media
|
||||
fetched on demand into a cache, reactions, pins, polls, search in a chat or everywhere, members and profiles,
|
||||
threads and a channel's comments, join by @name or link, administrator actions, and a mute that follows the
|
||||
account. Not supported: secret chats, calls, stories, payments; sign-in by QR code, folders, forum topics,
|
||||
drafts and scheduled messages are not done yet. The code is `nobilis/src/backend/telegram/`.
|
||||
|
||||
### Notifications are cards
|
||||
|
||||
Anything a service *says* rather than somebody *saying* - a join or part, a topic or mode change, a kick,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# Maintainer: Salastil <[email protected]>
|
||||
pkgname=moho-git
|
||||
pkgver=r1.268751e
|
||||
pkgrel=1
|
||||
pkgdesc="Unified desktop chat client for IRC, Discord, Sneedchat and Matrix"
|
||||
pkgdesc="Unified desktop chat client for IRC, Discord, Matrix, Kick, Sneedchat and Steam"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Moho-Chat/moho"
|
||||
license=('GPL3')
|
||||
|
||||
+1
-1
Submodule nobilis updated: 0f5003c061...5ab8eafceb
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The Telegram parts of the window, in a scratch moho (nothing is typed into a form that would be sent, so nothing
|
||||
* reaches Telegram): the account picker offers Telegram with its mark, its form asks for the api_id, the api_hash and
|
||||
* the phone number, explains where the first two come from, keeps the button off until all three are given, and
|
||||
* warns about Tor.
|
||||
*
|
||||
* node scripts/check-telegram-ui.mjs <output directory>
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { launchApp, sleep } from './lib/harness.mjs'
|
||||
|
||||
const out = process.argv[2] ?? '.'
|
||||
fs.mkdirSync(out, { recursive: true })
|
||||
const { page, stop } = await launchApp({ env: { MOHO_UI_SHOTS: '1' } })
|
||||
const ev = (code) => page.evaluate(code)
|
||||
const shot = async (name) => {
|
||||
const { data } = await page.call('Page.captureScreenshot', { format: 'png' })
|
||||
fs.writeFileSync(path.join(out, name), Buffer.from(data, 'base64'))
|
||||
}
|
||||
const results = []
|
||||
const check = (what, ok, detail = '') => {
|
||||
results.push(ok)
|
||||
console.log(ok ? ' ok ' : ' FAIL', what, detail)
|
||||
}
|
||||
try {
|
||||
await sleep(2500)
|
||||
await page.call('Emulation.setDeviceMetricsOverride', { width: 1100, height: 800, deviceScaleFactor: 1, mobile: false })
|
||||
await ev(`window.__mohoShots.store.setActivePanel('accounts')`)
|
||||
await sleep(1200)
|
||||
const chip = await ev(`(() => { const c = [...document.querySelectorAll('.service-chip')].find((x) => x.textContent.includes('Telegram')); return c ? { text: c.textContent.trim(), mark: !!c.querySelector('.mask-icon, img, span[style*="mask"]') } : null })()`)
|
||||
check('the picker offers Telegram', !!chip, JSON.stringify(chip))
|
||||
await ev(`[...document.querySelectorAll('.service-chip')].find((x) => x.textContent.includes('Telegram')).click()`)
|
||||
await sleep(500)
|
||||
const form = await ev(`(() => { const f = document.querySelector('.add-form'); return f ? f.innerText : '' })()`)
|
||||
check('the form says where the api_id comes from', /my\.telegram\.org\/apps/.test(form), form.slice(0, 120))
|
||||
check('it asks for the api_id, the api_hash and the phone number', /api_id/.test(form) && /api_hash/.test(form) && /Phone number/.test(form))
|
||||
const disabled = () => ev(`[...document.querySelectorAll('.add-form button.primary')].find((b) => /Send the code/.test(b.textContent))?.disabled`)
|
||||
check('nothing can be sent while the form is empty', (await disabled()) === true)
|
||||
const fill = (placeholderOrLabel, value) => ev(`(() => {
|
||||
const label = [...document.querySelectorAll('.add-form label.field')].find((l) => l.textContent.includes(${JSON.stringify(placeholderOrLabel)}))
|
||||
const input = label.querySelector('input')
|
||||
const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set
|
||||
set.call(input, ${JSON.stringify(value)})
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
})()`)
|
||||
await fill('api_id', '12345')
|
||||
await fill('api_hash', 'abcdef')
|
||||
check('two of three are not enough', (await disabled()) === true)
|
||||
await fill('Phone number', '+1 555 123 4567')
|
||||
await sleep(200)
|
||||
check('all three turn the button on', (await disabled()) === false)
|
||||
await ev(`document.querySelector('.add-form .switch')?.click()`)
|
||||
await sleep(300)
|
||||
const warning = await ev(`document.querySelector('.add-form .warning-note')?.innerText || ''`)
|
||||
check('routing it says Telegram may challenge a Tor sign-in', /Telegram/.test(warning), warning.slice(0, 80))
|
||||
await shot('telegram-form.png')
|
||||
} finally {
|
||||
await stop()
|
||||
console.log(results.every(Boolean) ? 'ALL OK' : 'SOME FAILED')
|
||||
process.exitCode = results.every(Boolean) ? 0 : 1
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<!-- Telegram's paper plane as a plain silhouette (Simple Icons, CC0). -->
|
||||
<path fill="#fff" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 825 B |
@@ -25,7 +25,7 @@ import type { Account } from '../../../shared/wire'
|
||||
* that gains a backend appears here as soon as it gains a form, and one the
|
||||
* daemon stops offering disappears without this file being touched.
|
||||
*/
|
||||
const FORMS = ['irc', 'discord', 'sneedchat', 'matrix', 'kick', 'steam', 'twitch', 'telegram'] as const
|
||||
const FORMS = ['irc', 'discord', 'sneedchat', 'matrix', 'kick', 'steam', 'twitch'] as const
|
||||
type AddableService = (typeof FORMS)[number]
|
||||
|
||||
/** What `listProtocols` answers with. */
|
||||
@@ -138,7 +138,6 @@ export function AccountsPanel(): JSX.Element {
|
||||
{adding === 'kick' && <KickForm onDone={() => setAdding(null)} />}
|
||||
{adding === 'steam' && <SteamForm onDone={() => setAdding(null)} />}
|
||||
{adding === 'twitch' && <TwitchForm onDone={() => setAdding(null)} />}
|
||||
{adding === 'telegram' && <TelegramForm onDone={() => setAdding(null)} />}
|
||||
{adding && (
|
||||
<button type="button" className="button subtle small" onClick={() => setAdding(null)}>
|
||||
Cancel
|
||||
@@ -1477,213 +1476,6 @@ function SteamForm({ onDone }: { onDone: () => void }): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding a Telegram account: an api_id and api_hash, a phone number, then the code Telegram sends and, if the account
|
||||
* has one, its cloud password.
|
||||
*
|
||||
* Telegram gives one api_id and api_hash to a phone number's owner, and every client sends them. moho has none of its
|
||||
* own, so the ones typed here are kept with the account, as private as the session is. Nothing typed here is sent
|
||||
* anywhere but Telegram.
|
||||
*/
|
||||
function TelegramForm({ onDone }: { onDone: () => void }): JSX.Element {
|
||||
const store = useStore()
|
||||
const [apiId, setApiId] = useState('')
|
||||
const [apiHash, setApiHash] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
const [useTor, setUseTor] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [loginId, setLoginId] = useState('')
|
||||
const [detail, setDetail] = useState('')
|
||||
const [needs, setNeeds] = useState<{ step: 'code' | 'password'; hint?: string; error?: string } | null>(null)
|
||||
const [typed, setTyped] = useState('')
|
||||
|
||||
// Held in a ref as well: the first event can be handled before the render that follows the answer that started the
|
||||
// sign-in. Nothing else is signing in from this form, so an event with no id known yet is this one's.
|
||||
const current = useRef('')
|
||||
const setId = (id: string): void => {
|
||||
current.current = id
|
||||
setLoginId(id)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const off = window.moho.onEvent((e) => {
|
||||
const d = e.data as { loginId?: string; text?: string; step?: 'code' | 'password'; hint?: string; error?: string; success?: boolean }
|
||||
if (!e.event.startsWith('telegramLogin')) return
|
||||
if (current.current && d.loginId !== current.current) return
|
||||
if (e.event === 'telegramLoginStatus') {
|
||||
setDetail(d.text || '')
|
||||
} else if (e.event === 'telegramLoginNeeds') {
|
||||
setNeeds({ step: d.step ?? 'code', hint: d.hint ?? undefined, error: d.error ?? undefined })
|
||||
setTyped('')
|
||||
setDetail('')
|
||||
} else if (e.event === 'telegramLoginResult') {
|
||||
setNeeds(null)
|
||||
setId('')
|
||||
setBusy(false)
|
||||
setDetail('')
|
||||
if (d.success) {
|
||||
setApiHash('')
|
||||
void store.refreshAccounts()
|
||||
onDone()
|
||||
} else {
|
||||
store.toast('error', d.error || 'Telegram sign-in failed')
|
||||
}
|
||||
}
|
||||
})
|
||||
return off
|
||||
}, [onDone, store])
|
||||
|
||||
// Closing the form on a sign-in that is still waiting tells the daemon nobody is.
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (current.current) void window.moho.rpc('cancelTelegramLogin', { loginId: current.current }).catch(() => {})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const begin = (): void => {
|
||||
setBusy(true)
|
||||
setDetail('')
|
||||
setNeeds(null)
|
||||
current.current = ''
|
||||
void window.moho
|
||||
.rpc<{ loginId: string }>('addTelegramAccount', { apiId: apiId.trim(), apiHash: apiHash.trim(), phone, useTor })
|
||||
.then((r) => setId(r.loginId))
|
||||
.catch((e: Error) => {
|
||||
setBusy(false)
|
||||
store.toast('error', e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const send = (): void => {
|
||||
const value = needs?.step === 'password' ? typed : typed.trim()
|
||||
if (!value || !loginId || !needs) return
|
||||
void window.moho
|
||||
.rpc(needs.step === 'password' ? 'submitTelegramPassword' : 'submitTelegramCode', { loginId, value })
|
||||
.then(() => setDetail('checking'))
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
}
|
||||
|
||||
const cancel = (): void => {
|
||||
if (current.current) void window.moho.rpc('cancelTelegramLogin', { loginId: current.current }).catch(() => {})
|
||||
setId('')
|
||||
setNeeds(null)
|
||||
setDetail('')
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
if (needs) {
|
||||
const password = needs.step === 'password'
|
||||
return (
|
||||
<div className="add-form">
|
||||
<p>
|
||||
{password
|
||||
? 'This account has a cloud password.'
|
||||
: 'Telegram sent a code to your other Telegram app, or by SMS.'}
|
||||
</p>
|
||||
<label className="field">
|
||||
<span className="small muted">{password ? `Password${needs.hint ? ` (hint: ${needs.hint})` : ''}` : 'Code'}</span>
|
||||
<input
|
||||
className="text-field"
|
||||
type={password ? 'password' : 'text'}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
inputMode={password ? undefined : 'numeric'}
|
||||
value={typed}
|
||||
onChange={(e) => setTyped(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') send()
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{needs.error && (
|
||||
<div className="warning-note">
|
||||
<Icon name="warning" size={16} />
|
||||
<span>{needs.error}</span>
|
||||
</div>
|
||||
)}
|
||||
{detail && <p className="small muted">{detail}…</p>}
|
||||
<div className="field-row">
|
||||
<button type="button" className="button primary" disabled={!typed.trim()} onClick={send}>
|
||||
{password ? 'Submit password' : 'Submit code'}
|
||||
</button>
|
||||
<button type="button" className="button" onClick={cancel}>
|
||||
Cancel sign-in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ready = apiId.trim() !== '' && apiHash.trim() !== '' && phone.trim() !== ''
|
||||
return (
|
||||
<div className="add-form">
|
||||
<p className="small muted">
|
||||
Telegram gives one <strong>api_id</strong> and <strong>api_hash</strong> to the owner of a phone number, at{' '}
|
||||
<a href="https://my.telegram.org/apps" target="_blank" rel="noreferrer">
|
||||
my.telegram.org/apps
|
||||
</a>
|
||||
, and every Telegram app a person uses can share them. moho has none of its own: the ones you type here are kept
|
||||
with the account, as private as the session. Telegram then sends a code, and asks for the account's cloud
|
||||
password if it has one.
|
||||
</p>
|
||||
<TorSwitch service="telegram" on={useTor} onChange={setUseTor} />
|
||||
<div className="field-row">
|
||||
<label className="field">
|
||||
<span className="small muted">api_id</span>
|
||||
<input
|
||||
className="text-field"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
inputMode="numeric"
|
||||
value={apiId}
|
||||
disabled={busy}
|
||||
onChange={(e) => setApiId(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="small muted">api_hash</span>
|
||||
<input
|
||||
className="text-field"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={apiHash}
|
||||
disabled={busy}
|
||||
onChange={(e) => setApiHash(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="field">
|
||||
<span className="small muted">Phone number, with its country code</span>
|
||||
<input
|
||||
className="text-field"
|
||||
autoComplete="off"
|
||||
placeholder="+1 555 123 4567"
|
||||
value={phone}
|
||||
disabled={busy}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && ready && !busy) begin()
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="field-row">
|
||||
<button type="button" className="button primary" disabled={busy || !ready} onClick={begin}>
|
||||
{busy ? 'Signing in…' : 'Send the code'}
|
||||
</button>
|
||||
{busy && (
|
||||
<button type="button" className="button" onClick={cancel}>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{busy && detail && <p className="small muted">{detail}…</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding a Twitch account: sign in, or only read.
|
||||
*
|
||||
@@ -1922,9 +1714,6 @@ function TorWarning({ service }: { service: Account['service'] }): JSX.Element |
|
||||
} else if (service === 'kick') {
|
||||
text =
|
||||
"Kick blocks Tor's exit points, so a connection through moho's own Tor will most likely be refused. A SOCKS5 proxy you host yourself may work."
|
||||
} else if (service === 'telegram') {
|
||||
text =
|
||||
"Telegram often challenges or limits a sign-in that comes from Tor or a VPN, and signing in from a new address can put the account's other sessions on alert. Sign in once from your usual connection, then route it if you need to."
|
||||
} else if (service === 'irc' || service === 'matrix') {
|
||||
text = `Each ${service === 'irc' ? 'network' : 'homeserver'} has its own policy on Tor, and many block its exit points. A SOCKS5 proxy you host yourself may be accepted where Tor is not.`
|
||||
} else {
|
||||
|
||||
@@ -303,7 +303,7 @@ export function BufferList(): JSX.Element {
|
||||
/** The call that sets a mute on the account itself, for a service that keeps one. */
|
||||
const accountMute = (accountId: string): string | null => {
|
||||
const service = accounts.find((a) => a.id === accountId)?.service
|
||||
return service === 'matrix' ? 'setMatrixRoomMuted' : service === 'discord' ? 'setDiscordChannelMuted' : service === 'steam' ? 'steamSetFriendMuted' : service === 'telegram' ? 'setTelegramChatMuted' : null
|
||||
return service === 'matrix' ? 'setMatrixRoomMuted' : service === 'discord' ? 'setDiscordChannelMuted' : service === 'steam' ? 'steamSetFriendMuted' : null
|
||||
}
|
||||
const isEffectivelyMuted = (buffer: BufferEntry): boolean =>
|
||||
isMutedBuffer(buffer, muted, mutedGroups)
|
||||
|
||||
@@ -260,7 +260,7 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
|
||||
// here rather than left as the one service where the button is dead.
|
||||
// Steam takes a picture or a short video, and says which and how big.
|
||||
const supportsAttachments =
|
||||
service === 'discord' || service === 'sneedchat' || service === 'matrix' || service === 'irc' || service === 'steam' || service === 'telegram'
|
||||
service === 'discord' || service === 'sneedchat' || service === 'matrix' || service === 'irc' || service === 'steam'
|
||||
|
||||
// Starting a reply puts the cursor in the box: the next thing anybody does
|
||||
// is write the answer.
|
||||
@@ -487,9 +487,6 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
|
||||
disabled: !supportsAttachments,
|
||||
onClick: pickAndStage
|
||||
},
|
||||
...(service === 'telegram' && !thread
|
||||
? ([{ label: 'Start a poll', icon: 'ballot', onClick: () => setPolling(true) }] as MenuEntry[])
|
||||
: []),
|
||||
...(service === 'matrix' && !thread
|
||||
? ([
|
||||
{
|
||||
@@ -847,8 +844,7 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
|
||||
// Somewhere that can be read and not written in: a channel this account has
|
||||
// no permission to send to, or Discord's own notices. A box that every send
|
||||
// is refused from is a worse answer than saying so where it would be.
|
||||
// A channel's comments are written to its discussion group, which the channel's own lock says nothing about.
|
||||
if (buffer.readOnly && !(thread && service === 'telegram')) {
|
||||
if (buffer.readOnly) {
|
||||
const official = buffer.readOnly.includes('official Discord')
|
||||
return (
|
||||
<div className="composer">
|
||||
@@ -1052,7 +1048,7 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
|
||||
{/* On the card's corner, and there only while it is pointed at
|
||||
or focused - they would be a dozen buttons otherwise. */}
|
||||
<div className="staged-actions">
|
||||
{(service === 'discord' || service === 'steam' || service === 'telegram') && (
|
||||
{(service === 'discord' || service === 'steam') && (
|
||||
<button
|
||||
type="button"
|
||||
className={classes('staged-action', att.spoiler && 'on')}
|
||||
@@ -1236,7 +1232,7 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
!text &&
|
||||
(service === 'discord' || service === 'sneedchat' || service === 'matrix' || service === 'telegram')
|
||||
(service === 'discord' || service === 'sneedchat' || service === 'matrix')
|
||||
) {
|
||||
const last = store.lastEditableOwn(buffer.id)
|
||||
if (last) {
|
||||
@@ -1350,7 +1346,7 @@ export function Composer({ thread = false }: { thread?: boolean } = {}): JSX.Ele
|
||||
messages at all: Discord's flag and base64 waveform, and Matrix's
|
||||
MSC3245 marker with integers. The daemon speaks both from one
|
||||
recording, so this is the same button either way. */}
|
||||
{!thread && (service === 'discord' || service === 'matrix' || service === 'telegram') && <VoiceRecorder bufferId={buffer.id} />}
|
||||
{!thread && (service === 'discord' || service === 'matrix') && <VoiceRecorder bufferId={buffer.id} />}
|
||||
|
||||
{/* Sending is the one thing the box does that its own Enter does not
|
||||
say, so it is only drawn when there is something to send. */}
|
||||
|
||||
@@ -72,17 +72,17 @@ export function ConversationMenu({ buffer }: { buffer: BufferEntry }): JSX.Eleme
|
||||
onTogglePin: () => togglePin(buffer.id),
|
||||
onToggleMute: () => {
|
||||
toggleMute(buffer.id)
|
||||
if (account?.service === 'matrix' || account?.service === 'steam' || account?.service === 'telegram') {
|
||||
if (account?.service === 'matrix' || account?.service === 'steam') {
|
||||
void window.moho
|
||||
.rpc(account.service === 'steam' ? 'steamSetFriendMuted' : account.service === 'telegram' ? 'setTelegramChatMuted' : 'setMatrixRoomMuted', { bufferId: buffer.id, muted: !isMuted(buffer.id) })
|
||||
.rpc(account.service === 'steam' ? 'steamSetFriendMuted' : 'setMatrixRoomMuted', { bufferId: buffer.id, muted: !isMuted(buffer.id) })
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
}
|
||||
},
|
||||
onUnmuteAccount:
|
||||
account?.service === 'matrix' || account?.service === 'steam' || account?.service === 'telegram'
|
||||
account?.service === 'matrix' || account?.service === 'steam'
|
||||
? () =>
|
||||
void window.moho
|
||||
.rpc(account.service === 'steam' ? 'steamSetFriendMuted' : account.service === 'telegram' ? 'setTelegramChatMuted' : 'setMatrixRoomMuted', { bufferId: buffer.id, muted: false })
|
||||
.rpc(account.service === 'steam' ? 'steamSetFriendMuted' : 'setMatrixRoomMuted', { bufferId: buffer.id, muted: false })
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
: undefined,
|
||||
onToggleAutojoin:
|
||||
|
||||
@@ -615,7 +615,6 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
// to a name half written.
|
||||
const filterMenu = suggestions(query)
|
||||
const isMatrix = buffer.accountId.startsWith('matrix:')
|
||||
const isTelegram = buffer.accountId.startsWith('telegram:')
|
||||
const isSteam = buffer.accountId.startsWith('steam:')
|
||||
const stream = useChat((s) => s.kickStreams)[buffer.id]
|
||||
// Whether this window is the one playing this channel. Per window rather
|
||||
@@ -683,11 +682,11 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
if (filtered) setResults([])
|
||||
})
|
||||
}
|
||||
if (!isMatrix && !isTelegram) return
|
||||
// Then the server, which has what this client never downloaded
|
||||
if (!isMatrix) return
|
||||
// Then the homeserver, which has what this client never downloaded
|
||||
// - including everything said before this account joined.
|
||||
return window.moho
|
||||
.rpc<MatrixSearch>(isTelegram ? 'searchTelegramMessages' : 'searchMatrixMessages', {
|
||||
.rpc<MatrixSearch>('searchMatrixMessages', {
|
||||
bufferId: buffer.id,
|
||||
query: text,
|
||||
scope,
|
||||
@@ -712,7 +711,7 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
.finally(() => setSearching(false))
|
||||
}, 250)
|
||||
return () => clearTimeout(timer)
|
||||
}, [query, buffer.id, store, isMatrix, isTelegram, isDiscord, scope])
|
||||
}, [query, buffer.id, store, isMatrix, isDiscord, scope])
|
||||
|
||||
// A viewer count that never moves is worse than none, and Kick pushes a
|
||||
// stream starting and stopping but never the count. The followed channels
|
||||
@@ -981,7 +980,7 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
|
||||
{isMatrix && buffer.kind !== 'server' && <ThreadList buffer={buffer} />}
|
||||
{isDiscord && buffer.kind === 'channel' && <DiscordThreads buffer={buffer} />}
|
||||
{(isMatrix || isDiscord || buffer.accountId.startsWith('telegram:')) && buffer.kind !== 'server' && <PinnedMessages buffer={buffer} />}
|
||||
{(isMatrix || isDiscord) && buffer.kind !== 'server' && <PinnedMessages buffer={buffer} />}
|
||||
|
||||
{/* Beside the pins, because it is the same kind of fact about a room:
|
||||
something the room keeps, rather than something said in it. */}
|
||||
@@ -1086,7 +1085,7 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
{/* Where to look. Only on Matrix, because it is the only protocol
|
||||
here whose server will answer the question - and the room is
|
||||
often exactly what somebody searching has forgotten. */}
|
||||
{(isMatrix || isTelegram) && (
|
||||
{isMatrix && (
|
||||
<div className="search-scope small">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -76,8 +76,6 @@ export function JoinPanel(): JSX.Element {
|
||||
return <KickJoin account={account} />
|
||||
case 'twitch':
|
||||
return <TwitchJoin account={account} />
|
||||
case 'telegram':
|
||||
return <TelegramJoin account={account} />
|
||||
case 'sneedchat':
|
||||
return <SneedchatRooms account={account} />
|
||||
case 'steam':
|
||||
@@ -86,7 +84,7 @@ export function JoinPanel(): JSX.Element {
|
||||
return <div className="panel muted">Joining isn't supported for this service yet.</div>
|
||||
}
|
||||
})()
|
||||
const joinable = all.filter((a) => ['irc', 'matrix', 'discord', 'kick', 'sneedchat', 'steam', 'twitch', 'telegram'].includes(a.service))
|
||||
const joinable = all.filter((a) => ['irc', 'matrix', 'discord', 'kick', 'sneedchat', 'steam', 'twitch'].includes(a.service))
|
||||
|
||||
return (
|
||||
<div className="join-frame">
|
||||
@@ -382,24 +380,6 @@ function TwitchJoin({ account }: { account: Account }): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
function TelegramJoin({ account }: { account: Account }): JSX.Element {
|
||||
const store = useStore()
|
||||
return (
|
||||
<div className="panel join-panel">
|
||||
<SubmitField
|
||||
label="Join a chat, or open a conversation"
|
||||
placeholder="@name, or a t.me link"
|
||||
onSubmit={(name) => void store.joinAndOpen(account.id, name)}
|
||||
/>
|
||||
<p className="small muted">
|
||||
A public group or channel is joined by its @name or t.me link, a private one by the invite link
|
||||
somebody gave you, and a person's @name opens a conversation with them. Telegram may ask an
|
||||
administrator to accept a request to join. Closing a chat in the list leaves it.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** One person in a friend request. */
|
||||
interface SteamPerson {
|
||||
steamid: string
|
||||
|
||||
@@ -256,16 +256,14 @@ export function MessageList(): JSX.Element {
|
||||
* whose replies are older than the loaded window, which is the honest
|
||||
* failure - it says "at least this many", never "none" for a live one.
|
||||
*/
|
||||
const told = useChat((s) => (bufferId ? s.threadCounts[bufferId] : undefined))
|
||||
const threadReplies = useMemo(() => {
|
||||
// What the service says wins where it says: a Telegram channel's comments are in another chat altogether.
|
||||
const counts: Record<string, number> = { ...(told ?? {}) }
|
||||
const counts: Record<string, number> = {}
|
||||
for (const m of all) {
|
||||
const root = m.replyTo?.thread ? m.replyTo.id : m.replyTo?.threadRoot
|
||||
if (root && !(told && root in told)) counts[root] = (counts[root] ?? 0) + 1
|
||||
if (root) counts[root] = (counts[root] ?? 0) + 1
|
||||
}
|
||||
return counts
|
||||
}, [all, told])
|
||||
}, [all])
|
||||
|
||||
const messages = useMemo(() => {
|
||||
const allowed: Record<string, boolean> = {
|
||||
|
||||
@@ -409,8 +409,8 @@ function MessageRowBody({
|
||||
// Discord, Sneedchat and Matrix support editing and deleting your own
|
||||
// messages; IRC has no such concept.
|
||||
const canEditDelete =
|
||||
!!message.isOwn && (service === 'discord' || service === 'sneedchat' || service === 'matrix' || service === 'telegram')
|
||||
const canReact = service === 'discord' || service === 'matrix' || service === 'steam' || service === 'telegram'
|
||||
!!message.isOwn && (service === 'discord' || service === 'sneedchat' || service === 'matrix')
|
||||
const canReact = service === 'discord' || service === 'matrix' || service === 'steam'
|
||||
// Steam's stickers, asked for when the reaction picker opens: it reacts with them too.
|
||||
const [steamStickers, setSteamStickers] = useState<StickerEntry[]>([])
|
||||
useEffect(() => {
|
||||
@@ -641,7 +641,7 @@ function MessageRowBody({
|
||||
// rather than gated on a power level read here: the server decides, and
|
||||
// it refuses in words worth showing - hiding the action from somebody who
|
||||
// could have used it is the worse mistake.
|
||||
...((service === 'matrix' || service === 'discord' || service === 'telegram') && !isSystem
|
||||
...((service === 'matrix' || service === 'discord') && !isSystem
|
||||
? ([
|
||||
{
|
||||
label: pinned ? 'Unpin from this conversation' : 'Pin to this conversation',
|
||||
@@ -1742,15 +1742,12 @@ function moderationEntries(
|
||||
// display names collide and can be changed at will.
|
||||
const userId = message.senderId
|
||||
if (!userId) return []
|
||||
const telegram = store.accountFor(bufferId)?.service === 'telegram'
|
||||
const call = (method: string): void => {
|
||||
const account = store.accountFor(bufferId)
|
||||
if (!account) return
|
||||
// Telegram's three are one call that names which.
|
||||
const asked = telegram
|
||||
? window.moho.rpc('moderateTelegramMember', { bufferId, userId, action: method.replace('MatrixMember', '') })
|
||||
: window.moho.rpc(method, { accountId: account.id, bufferId, userId })
|
||||
void asked.catch((e: Error) => store.toast('error', e.message))
|
||||
void window.moho
|
||||
.rpc(method, { accountId: account.id, bufferId, userId })
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
}
|
||||
const out: MenuEntry[] = []
|
||||
if (permissions.canMute)
|
||||
@@ -1761,7 +1758,7 @@ function moderationEntries(
|
||||
out.push({ label: 'Ban sender', icon: 'gavel', danger: true, onClick: () => call('banMatrixMember') })
|
||||
// Reachable only from here, not the member list: a banned user is no
|
||||
// longer a member, so their scrollback is the one place left to find them.
|
||||
if (!telegram) out.push({ label: 'Unban sender', icon: 'lock_open', onClick: () => call('unbanMatrixMember') })
|
||||
out.push({ label: 'Unban sender', icon: 'lock_open', onClick: () => call('unbanMatrixMember') })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -381,14 +381,6 @@ export function NickList(): JSX.Element {
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
return
|
||||
}
|
||||
if (account.service === 'telegram') {
|
||||
if (action !== 'kick' && action !== 'ban' && action !== 'mute') return
|
||||
void window.moho
|
||||
.rpc('moderateTelegramMember', { bufferId: buffer.id, userId: member.userId, action })
|
||||
.then(() => store.toast('info', `${action === 'ban' ? 'Banned' : action === 'mute' ? 'Muted for 10 minutes' : 'Removed'} ${member.nick}`))
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
return
|
||||
}
|
||||
if (account.service === 'steam') {
|
||||
const group = steamRoomIds(buffer)?.group
|
||||
if (!group || !member.userId || (action !== 'kick' && action !== 'ban' && action !== 'mute')) return
|
||||
|
||||
@@ -89,7 +89,7 @@ function PinnedMessagesBanner({ bufferId }: { bufferId: string }): JSX.Element |
|
||||
const rows = useChat((s) => s.pinnedRows)[bufferId]
|
||||
const [at, setAt] = useState(0)
|
||||
const [, redraw] = useState(0)
|
||||
const hasPins = account.startsWith('discord:') || account.startsWith('matrix:') || account.startsWith('telegram:')
|
||||
const hasPins = account.startsWith('discord:') || account.startsWith('matrix:')
|
||||
|
||||
useEffect(() => {
|
||||
setAt(0)
|
||||
|
||||
@@ -5,8 +5,6 @@ import { useStore } from '../state/hooks'
|
||||
|
||||
/** The spec's own ceiling, so twenty-one answers is refused here not there. */
|
||||
const MAX_ANSWERS = 20
|
||||
/** Telegram's: ten answers, and a count that can be held back until the poll closes. */
|
||||
const TELEGRAM_MAX_ANSWERS = 10
|
||||
|
||||
/**
|
||||
* Starting a poll.
|
||||
@@ -20,7 +18,6 @@ const TELEGRAM_MAX_ANSWERS = 10
|
||||
*/
|
||||
export function PollComposer({ bufferId, onClose }: { bufferId: string; onClose: () => void }): JSX.Element {
|
||||
const store = useStore()
|
||||
const max = store.accountFor(bufferId)?.service === 'telegram' ? TELEGRAM_MAX_ANSWERS : MAX_ANSWERS
|
||||
const [question, setQuestion] = useState('')
|
||||
const [answers, setAnswers] = useState(['', ''])
|
||||
const [disclosed, setDisclosed] = useState(true)
|
||||
@@ -80,7 +77,7 @@ export function PollComposer({ bufferId, onClose }: { bufferId: string; onClose:
|
||||
<button
|
||||
type="button"
|
||||
className="button subtle small"
|
||||
disabled={answers.length >= max}
|
||||
disabled={answers.length >= MAX_ANSWERS}
|
||||
onClick={() => setAnswers([...answers, ''])}
|
||||
>
|
||||
Add an answer
|
||||
|
||||
@@ -127,7 +127,6 @@ const CATEGORIES: { id: string; label: string; group: string; icon?: string; ser
|
||||
{ id: 'matrix', label: 'Matrix', group: 'Networks', service: 'matrix' },
|
||||
{ id: 'kick', label: 'Kick', group: 'Networks', service: 'kick' },
|
||||
{ id: 'twitch', label: 'Twitch', group: 'Networks', service: 'twitch' },
|
||||
{ id: 'telegram', label: 'Telegram', group: 'Networks', service: 'telegram' },
|
||||
{ id: 'about', label: 'About', group: 'About' }
|
||||
]
|
||||
|
||||
@@ -248,8 +247,6 @@ function Page({ id }: { id: string }): JSX.Element | null {
|
||||
return <KickSettings />
|
||||
case 'twitch':
|
||||
return <TwitchSettings />
|
||||
case 'telegram':
|
||||
return <TelegramSettings />
|
||||
case 'about':
|
||||
return <AboutSettings />
|
||||
default:
|
||||
@@ -1265,32 +1262,6 @@ function KickSettings(): JSX.Element {
|
||||
* Twitch. What is here is what the plan (Gitea issue #55) has built so far:
|
||||
* where a channel's recent past comes from, and what is said about signing in.
|
||||
*/
|
||||
function TelegramSettings(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<SettingsSection
|
||||
title="Telegram"
|
||||
description="Each account signs in with its own api_id and api_hash, which Telegram gives you at my.telegram.org/apps, and one session that moho keeps and reuses. It never asks for a new code just because the connection dropped. Add an account from the Accounts pane."
|
||||
/>
|
||||
|
||||
<SettingsSection
|
||||
title="Where moho shows up"
|
||||
description="Telegram lists every session of an account under Settings → Devices in its own apps, named after this computer, for example “moho (linux x86_64)”. Ending it there signs moho out; moho then says so and stops, rather than signing in again."
|
||||
/>
|
||||
|
||||
<SettingsSection
|
||||
title="Muting and notifications"
|
||||
description="Muting a chat from its menu is Telegram's own mute for the account, so it holds on your phone as well, and a chat you muted on your phone is muted here."
|
||||
/>
|
||||
|
||||
<SettingsSection
|
||||
title="What moho does not do"
|
||||
description="Secret chats, voice and video calls, stories, payments and bots' inline mode are not part of moho. Signing in by QR code, folders, forum topics, drafts and scheduled messages are not done yet."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TwitchSettings(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -94,13 +94,3 @@ describe('markup details', () => {
|
||||
expect(wrap('sneedchat', 'color', 'g', '#00fc00')).toBe('[color=#00fc00]g[/color]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Telegram formatting', () => {
|
||||
it('is offered the formats the daemon turns into entities, written as Discord reads them', () => {
|
||||
expect(formatsFor('telegram')).toEqual(['bold', 'italic', 'underline', 'strike', 'code', 'spoiler', 'quote'])
|
||||
expect(wrap('telegram', 'bold', 'x')).toBe('**x**')
|
||||
expect(wrap('telegram', 'underline', 'x')).toBe('__x__')
|
||||
expect(wrap('telegram', 'spoiler', 'x')).toBe('||x||')
|
||||
expect(wrap('telegram', 'quote', 'a\nb')).toBe('\n> a\n> b\n')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,8 +21,6 @@ const CAPABILITIES: Record<string, FormatKind[]> = {
|
||||
discord: ['bold', 'italic', 'underline', 'strike', 'code', 'spoiler', 'quote'],
|
||||
// markup.rs in the daemon: bold, italic, strike and code.
|
||||
matrix: ['bold', 'italic', 'strike', 'code'],
|
||||
// Written as Markdown, the way Discord reads it, and turned into Telegram's entities by the daemon (telegram/markup.rs).
|
||||
telegram: ['bold', 'italic', 'underline', 'strike', 'code', 'spoiler', 'quote'],
|
||||
sneedchat: ['bold', 'italic', 'underline', 'strike', 'code', 'spoiler', 'quote', 'color'],
|
||||
// Quote is left out of IRC: a quote is a line of its own, and a newline
|
||||
// reaching IRC is a malformed message.
|
||||
@@ -131,7 +129,7 @@ export function wrap(service: string | undefined, kind: FormatKind, inner: strin
|
||||
const trail = inner.match(/\s*$/)?.[0] ?? ''
|
||||
const core = inner.slice(lead.length, inner.length - trail.length)
|
||||
const marked = (open: string, close = open): string => `${lead}${open}${core}${close}${trail}`
|
||||
const markdown = service === 'discord' || service === 'matrix' || service === 'telegram'
|
||||
const markdown = service === 'discord' || service === 'matrix'
|
||||
|
||||
if (service === 'sneedchat') {
|
||||
switch (kind) {
|
||||
@@ -174,11 +172,11 @@ export function wrap(service: string | undefined, kind: FormatKind, inner: strin
|
||||
const [open, close] = codeFence(core)
|
||||
return marked(open, close)
|
||||
}
|
||||
case 'underline': return service === 'discord' || service === 'telegram' ? marked('__') : inner
|
||||
case 'spoiler': return service === 'discord' || service === 'telegram' ? marked('||') : inner
|
||||
case 'underline': return service === 'discord' ? marked('__') : inner
|
||||
case 'spoiler': return service === 'discord' ? marked('||') : inner
|
||||
case 'quote': {
|
||||
if (service !== 'discord' && service !== 'telegram') return inner
|
||||
// A quote is a line of its own on Discord and Telegram, however it was typed.
|
||||
if (service !== 'discord') return inner
|
||||
// A quote is a line of its own on Discord, however it was typed.
|
||||
const quoted = core.split('\n').map((line) => `> ${line}`).join('\n')
|
||||
return `${lead}\n${quoted}\n${trail}`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { resolveMediaUrl } from './util'
|
||||
|
||||
/** The caches the daemon can restore from, by folder. Kick's emotes are
|
||||
* not here: lib/emotecache puts Kick's own picture up at once instead. */
|
||||
const RESTORABLE = /[\\/]nobilis[\\/](matrix-media|sneedchat-attachments|sneedchat-avatars|discord-thumbnails|discord-icons|telegram-media|telegram-avatars)[\\/]/
|
||||
const RESTORABLE = /[\\/]nobilis[\\/](matrix-media|sneedchat-attachments|sneedchat-avatars|discord-thumbnails|discord-icons)[\\/]/
|
||||
|
||||
/** Paths being fetched, so a picture drawn twice asks once. */
|
||||
const inFlight = new Map<string, Promise<string | null>>()
|
||||
|
||||
@@ -4,7 +4,6 @@ import matrixSvg from '../assets/matrix-mark.svg'
|
||||
import kickSvg from '../assets/kick.svg'
|
||||
import steamSvg from '../assets/steam-mark.svg'
|
||||
import twitchSvg from '../assets/twitch-mark.svg'
|
||||
import telegramSvg from '../assets/telegram-mark.svg'
|
||||
|
||||
/**
|
||||
* Discord channel buffers are named "GuildName/#channel" internally - that's
|
||||
@@ -63,7 +62,7 @@ export interface ServiceIcon {
|
||||
* the same menus, as "Whisper".
|
||||
*/
|
||||
export function hasDirectMessages(service: string | undefined): boolean {
|
||||
return service === 'irc' || service === 'discord' || service === 'matrix' || service === 'twitch' || service === 'telegram'
|
||||
return service === 'irc' || service === 'discord' || service === 'matrix' || service === 'twitch'
|
||||
}
|
||||
|
||||
export function serviceIcon(service: string): ServiceIcon {
|
||||
@@ -76,8 +75,6 @@ export function serviceIcon(service: string): ServiceIcon {
|
||||
return { mark: steamSvg }
|
||||
case 'twitch':
|
||||
return { mark: twitchSvg }
|
||||
case 'telegram':
|
||||
return { mark: telegramSvg }
|
||||
case 'irc':
|
||||
return { glyph: 'tag' }
|
||||
case 'slack':
|
||||
@@ -113,8 +110,6 @@ export function serviceLabel(service: string): string {
|
||||
return 'Steam'
|
||||
case 'twitch':
|
||||
return 'Twitch'
|
||||
case 'telegram':
|
||||
return 'Telegram'
|
||||
case 'slack':
|
||||
return 'Slack'
|
||||
case 'jabber':
|
||||
|
||||
@@ -600,8 +600,6 @@ export interface ChatState {
|
||||
/** Which messages each Matrix room has pinned, newest last. */
|
||||
/** Which messages a conversation has pinned, where the service says so. */
|
||||
pinnedMessages: Record<string, string[]>
|
||||
/** How many replies or comments a message has, where the service says so (Telegram): by conversation, then message. */
|
||||
threadCounts: Record<string, Record<string, number>>
|
||||
/**
|
||||
* The pinned messages themselves, once somebody has asked for them. The ids
|
||||
* above say that something is pinned; these say what, for the banner over
|
||||
@@ -829,7 +827,6 @@ const INITIAL: ChatState = {
|
||||
pinnedByBuffer: {},
|
||||
reviewCard: null,
|
||||
pinnedMessages: {},
|
||||
threadCounts: {},
|
||||
pinnedRows: {},
|
||||
forumPages: {},
|
||||
discordFriends: {},
|
||||
@@ -2682,8 +2679,6 @@ export class ChatStore {
|
||||
attachment,
|
||||
spoilers
|
||||
)
|
||||
// A Telegram comment lands in the channel's discussion group, not in this conversation, so the panel asks again.
|
||||
if (this.accountFor(open.bufferId)?.service === 'telegram') void this.openThreadPanel(open.bufferId, open.rootId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3647,14 +3642,6 @@ export class ChatStore {
|
||||
this.mapMessage(data.bufferId, data.id, (m) => ({ ...m, moderated: data.moderated ?? {} }))
|
||||
break
|
||||
|
||||
case 'threadCount': {
|
||||
const d = data as unknown as { bufferId: string; id: string; count: number }
|
||||
const mine = this.state.threadCounts[d.bufferId] ?? {}
|
||||
if (mine[d.id] === d.count) break
|
||||
this.set({ threadCounts: { ...this.state.threadCounts, [d.bufferId]: { ...mine, [d.id]: d.count } } })
|
||||
break
|
||||
}
|
||||
|
||||
case 'reactionsChanged':
|
||||
this.mapMessage(data.bufferId, data.id, (m) => ({ ...m, reactions: data.reactions }))
|
||||
break
|
||||
@@ -5445,15 +5432,6 @@ export class ChatStore {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (account?.service === 'telegram') {
|
||||
try {
|
||||
const perms = await window.moho.rpc<RoomPermissions>('telegramPermissions', { bufferId })
|
||||
this.set({ matrixPermissions: { ...this.state.matrixPermissions, [bufferId]: perms } })
|
||||
} catch {
|
||||
/* advisory only */
|
||||
}
|
||||
return
|
||||
}
|
||||
if (account?.service !== 'matrix') return
|
||||
try {
|
||||
const perms = await window.moho.rpc<RoomPermissions>('getMatrixRoomPermissions', {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
export interface Account {
|
||||
id: string
|
||||
service: 'irc' | 'discord' | 'sneedchat' | 'matrix' | 'kick' | 'steam' | 'twitch' | 'telegram' | 'jabber' | 'slack'
|
||||
service: 'irc' | 'discord' | 'sneedchat' | 'matrix' | 'kick' | 'steam' | 'twitch' | 'jabber' | 'slack'
|
||||
displayName: string
|
||||
/**
|
||||
* How the user is presenting: "online" | "idle". Distinct from
|
||||
|
||||
Reference in New Issue
Block a user