Stoat (#32): voice calls in the window, Stoat invite links join the server; nobilis pointer

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
SalastilandClaude Sonnet 5.5 committed 2026-10-23 22:10:04 -04:00
1 parent 1d674edfe6
commit df0edf1cf8
4 files changed
+60 -4

No files matched your search

+1 -1
Submodule nobilis updated: 9648ae56c8...39a65bd0df.
+12
View File
@@ -2281,6 +2281,18 @@ export class ChatStore {
return
}
// A Stoat invite joins the server, in the first Stoat account there is; with none, it is a web page for the browser.
if (link.service === 'stoat') {
const account = this.state.accounts.find((a) => a.service === 'stoat' && a.state === 'connected') ?? this.state.accounts.find((a) => a.service === 'stoat')
if (!account) {
void window.moho.openExternal(link.url)
return
}
this.set({ activePanel: '' })
void this.joinAndOpen(account.id, link.url)
return
}
// A Telegram link goes to wherever it points: the chat, joined if the account is not in it, and the post.
if (link.service === 'telegram') {
void this.openTelegramLink(link.url)
+15 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { isDeepLink, isTelegramLink, parseDeepLink } from './deeplink'
import { isDeepLink, isTelegramLink, parseDeepLink, stoatInviteCode } from './deeplink'
describe('a Telegram link', () => {
it('is what the View in Telegram button of a post opens, and what a message says', () => {
@@ -44,3 +44,17 @@ describe('a Telegram link', () => {
expect(parseDeepLink('https://kick.com/xqc')?.service).toBe('kick')
})
})
describe('a Stoat invite', () => {
it('is read from the official addresses, and only those', () => {
expect(stoatInviteCode('https://stoat.chat/invite/gJhyqXTt')).toBe('gJhyqXTt')
expect(stoatInviteCode('https://app.stoat.chat/invite/gJhyqXTt/')).toBe('gJhyqXTt')
expect(stoatInviteCode('https://rvlt.gg/Abc123')).toBe('Abc123')
expect(stoatInviteCode('https://stoat.chat/')).toBeNull()
expect(stoatInviteCode('https://example.com/invite/abc')).toBeNull()
expect(stoatInviteCode('https://stoat.chat.example.com/invite/abc')).toBeNull()
expect(stoatInviteCode('https://stoat.chat/invite/a b')).toBeNull()
expect(parseDeepLink('https://stoat.chat/invite/gJhyqXTt')).toEqual({ service: 'stoat', code: 'gJhyqXTt', url: 'https://stoat.chat/invite/gJhyqXTt' })
expect(isDeepLink('https://stoat.chat/invite/gJhyqXTt')).toBe(true)
})
})
+32 -2
View File
@@ -36,7 +36,14 @@ export interface TelegramLink {
url: string
}
export type DeepLink = IrcLink | KickLink | TelegramLink
/** An invitation to a Stoat server: its link in a message, which joins it where the account has one. */
export interface StoatInvite {
service: 'stoat'
code: string
url: string
}
export type DeepLink = IrcLink | KickLink | TelegramLink | StoatInvite
/**
* Schemes moho asks the desktop to send it.
@@ -50,7 +57,28 @@ export const DEEP_LINK_SCHEMES = ['irc', 'ircs', 'tg']
/** Whether a string is a link this client would act on. */
export function isDeepLink(value: string): boolean {
const lower = value.toLowerCase()
return DEEP_LINK_SCHEMES.some((s) => lower.startsWith(`${s}:`)) ? isTelegramOrIrc(value) : !!kickHandle(value) || isTelegramLink(value)
return DEEP_LINK_SCHEMES.some((s) => lower.startsWith(`${s}:`)) ? isTelegramOrIrc(value) : !!kickHandle(value) || isTelegramLink(value) || !!stoatInviteCode(value)
}
/**
* The code in a link to a Stoat invite (`stoat.chat/invite/Code`, the same on the web app or Revolt's old names, or the
* short `rvlt.gg/Code`), or null. Only the official addresses: a self-hosted instance's own are joined by pasting them
* into the join panel, since nothing here can tell its address from any other website's.
*/
export function stoatInviteCode(value: string): string | null {
let url: URL
try {
url = new URL(value.trim())
} catch {
return null
}
if (!/^https?:$/.test(url.protocol)) return null
const host = url.hostname.replace(/^www\./, '').toLowerCase()
const parts = url.pathname.split('/').filter(Boolean)
let code: string | undefined
if (['stoat.chat', 'app.stoat.chat', 'revolt.chat', 'app.revolt.chat'].includes(host) && parts.length === 2 && parts[0] === 'invite') code = parts[1]
else if (['rvlt.gg', 'stt.gg'].includes(host) && parts.length === 1) code = parts[0]
return code && /^[A-Za-z0-9_-]{1,32}$/.test(code) ? code : null
}
/** A `tg:` link is only one of ours if it is a kind that opens a chat; the others (login, proxy, themes) are not. */
@@ -143,6 +171,8 @@ export function parseDeepLink(value: string): DeepLink | null {
const kick = kickHandle(value)
if (kick) return { service: 'kick', handle: kick }
if (isTelegramLink(value)) return { service: 'telegram', url: value.trim() }
const invite = stoatInviteCode(value)
if (invite) return { service: 'stoat', code: invite, url: value.trim() }
let url: URL
try {