Compare commits

...

21 commits

Author SHA1 Message Date
52db5599c4 merged from upstream
Some checks are pending
Deploy to Netlify (dev) / Deploy to Netlify (push) Waiting to run
2026-02-23 15:22:46 +02:00
Krishan
6347640a35
Release v4.10.5 (#2692) 2026-02-23 23:05:01 +11:00
renovate[bot]
f2d8ad0b6b
chore(deps): update docker/metadata-action action to v5.10.0 (#2690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:58:20 +11:00
renovate[bot]
739786d9ab
chore(deps): update docker/setup-qemu-action action to v3.7.0 (#2691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:57:44 +11:00
renovate[bot]
f642809939
chore(deps): update docker/login-action action to v3.7.0 (#2689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:56:52 +11:00
Krishan
02106a99b9
Release v4.10.4 (#2688) 2026-02-23 22:32:06 +11:00
dependabot[bot]
df3a3ba789
Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#2641)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.11.1...v3.12.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 18:18:01 +11:00
dependabot[bot]
cd80d4c9e8
Bump docker/build-push-action from 6.18.0 to 6.19.2 (#2642)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.2.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.18.0...v6.19.2)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 18:13:17 +11:00
Krishan
dab44edef2
Add prod-deploy.yml to Docker PR workflow paths (#2687) 2026-02-23 18:12:37 +11:00
Ajay Bura
ed0ad61bc4
Verify SSO window message origin (#2686) 2026-02-23 18:08:25 +11:00
Ajay Bura
b2cb717178
fix noreferrer typo in url preview link (#2685) 2026-02-23 17:56:14 +11:00
dependabot[bot]
7a9f6d2223
Bump linkifyjs and linkify-react from 4.1.3 to 4.3.2 (#2682)
* Bump linkifyjs from 4.1.3 to 4.3.2

Bumps [linkifyjs](https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkifyjs) from 4.1.3 to 4.3.2.
- [Release notes](https://github.com/nfrasser/linkifyjs/releases)
- [Changelog](https://github.com/nfrasser/linkifyjs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nfrasser/linkifyjs/commits/v4.3.2/packages/linkifyjs)

---
updated-dependencies:
- dependency-name: linkifyjs
  dependency-version: 4.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* update linkify react

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
2026-02-23 17:43:15 +11:00
Ajay Bura
a9022184fc
Set message power to moderator in space (#2684) 2026-02-23 16:57:39 +11:00
renovate[bot]
826b3c2997
chore(deps): update actions/setup-node action to v6 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 18:57:23 +11:00
dependabot[bot]
2e6c5f7c04
Bump actions/upload-artifact from 4.6.2 to 6.0.0 (#2644)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 6.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.2...v6.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-22 18:26:54 +11:00
dependabot[bot]
2d6730de56
Bump actions/checkout from 4.2.0 to 6.0.2 (#2640)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.0 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.2.0...v6.0.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-22 18:26:08 +11:00
Krishan
b6cc0e3077
Update node to v24.13.1 LTS (#2622)
* Update node to v24.13.1 LTS

* Fix dockerfile node version

* Simplify node and nginx version, bump nginx

* Fix casing
2026-02-22 18:15:23 +11:00
Ajay Bura
91c8731940
Add permission for managing emojis & stickers (#2678)
add permission for managing emojis & stickers
2026-02-22 15:48:23 +11:00
renovate[bot]
1f03891b25
fix(deps): update dependency folds to v2.6.1 (#2679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 15:43:30 +11:00
Ajay Bura
9ff15b8b03
fix space lobby / search selected hook not working (#2675) 2026-02-22 15:14:04 +11:00
Ajay Bura
170f5cd473
Request session info from sw if missing (#2664)
* request session info from sw if missing

* fix async session request in fetch

* respond fetch synchronously and add early check for non media requests  (#2670)

* make sure we call respondWith synchronously

* simplify isMediaRequest in sw

* improve naming in sw

* get back baseUrl check into validMediaRequest

* pass original request into fetch in sw

* extract mediaPath util and performs checks properly

---------

Co-authored-by: mmmykhailo <35040944+mmmykhailo@users.noreply.github.com>
2026-02-21 17:51:27 +11:00
21 changed files with 195 additions and 88 deletions

View file

@ -12,11 +12,11 @@ jobs:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v6.0.2
- name: Setup node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v6.2.0
with:
node-version: 20.12.2
node-version: 24.13.1
cache: 'npm'
- name: Install dependencies
run: npm ci
@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v6.0.0
with:
name: preview
path: dist
@ -33,7 +33,7 @@ jobs:
- name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v6.0.0
with:
name: pr
path: ./pr.txt

View file

@ -5,15 +5,16 @@ on:
paths:
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
- '.github/workflows/prod-deploy.yml'
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v6.0.2
- name: Build Docker image
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.19.2
with:
context: .
push: false

View file

@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@v6.0.2
- name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with:

View file

@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v6.0.2
- name: Setup node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v6.2.0
with:
node-version: 20.12.2
node-version: 24.13.1
cache: 'npm'
- name: Install dependencies
run: npm ci

View file

@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v6.0.2
- name: Setup node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v6.2.0
with:
node-version: 20.12.2
node-version: 24.13.1
cache: 'npm'
- name: Install dependencies
run: npm ci
@ -66,31 +66,31 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
uses: docker/setup-qemu-action@v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@v3.12.0
- name: Login to Docker Hub
uses: docker/login-action@v3.6.0
uses: docker/login-action@v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry
uses: docker/login-action@v3.6.0
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@v5.10.0
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.19.2
with:
context: .
platforms: linux/amd64,linux/arm64

View file

@ -1,5 +1,5 @@
## Builder
FROM node:20.12.2-alpine3.18 as builder
FROM node:24.13.1-alpine AS builder
WORKDIR /src
@ -11,7 +11,7 @@ RUN npm run build
## App
FROM nginx:1.29.3-alpine
FROM nginx:1.29.5-alpine
COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf

View file

@ -83,7 +83,7 @@ mxFo+ioe/ABCufSmyqFye0psX3Sp
## Local development
> [!TIP]
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20).
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
Execute the following commands to start a development server:
```sh

30
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cinny",
"version": "4.10.3",
"version": "4.10.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
"version": "4.10.3",
"version": "4.10.5",
"license": "AGPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
@ -34,7 +34,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
"folds": "2.5.0",
"folds": "2.6.1",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@ -43,8 +43,8 @@
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
"linkify-react": "4.1.3",
"linkifyjs": "4.1.3",
"linkify-react": "4.3.2",
"linkifyjs": "4.3.2",
"matrix-js-sdk": "38.2.0",
"matrix-widget-api": "1.11.0",
"millify": "6.1.0",
@ -7197,9 +7197,9 @@
}
},
"node_modules/folds": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/folds/-/folds-2.5.0.tgz",
"integrity": "sha512-UJhvXAQ1XnZ9w10KJwSW+frvzzWE/zcF0dH3fDVCD70RFHAxwEi0UkkVS8CaZGxZF2Wvt3qTJyTS5LW3LwwUAw==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/folds/-/folds-2.6.1.tgz",
"integrity": "sha512-0L1ZSqwjFSg2fesa//C4DgP47Vp/KqDuzjAaOEYN21AvoptyVI+6OEXWrtIdE8DPQCZYr0bV+tqbrLyA6uAhaw==",
"license": "Apache-2.0",
"peerDependencies": {
"@vanilla-extract/css": "1.9.2",
@ -8531,18 +8531,20 @@
}
},
"node_modules/linkify-react": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
"integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.3.2.tgz",
"integrity": "sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==",
"license": "MIT",
"peerDependencies": {
"linkifyjs": "^4.0.0",
"react": ">= 15.0.0"
}
},
"node_modules/linkifyjs": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz",
"integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg=="
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "cinny",
"version": "4.10.3",
"version": "4.10.5",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
@ -46,7 +46,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
"folds": "2.5.0",
"folds": "2.6.1",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@ -55,9 +55,9 @@
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
"linkify-react": "4.1.3",
"linkifyjs": "4.1.3",
"matrix-widget-api": "1.11.0",
"linkify-react": "4.3.2",
"linkifyjs": "4.3.2",
"matrix-js-sdk": "38.2.0",
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
@ -113,4 +113,4 @@
"vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.4"
}
}
}

View file

@ -75,6 +75,10 @@ export const createRoomParentState = (parent: Room) => ({
},
});
const createSpacePowerLevelsOverride = () => ({
events_default: 50,
});
export const createRoomEncryptionState = () => ({
type: 'm.room.encryption',
state_key: '',
@ -165,6 +169,10 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
initial_state: initialState,
};
if (data.type === RoomType.Space) {
options.power_level_content_override = createSpacePowerLevelsOverride();
}
const result = await mx.createRoom(options);
if (data.parent) {

View file

@ -26,7 +26,12 @@ export function SSOStage({
useEffect(() => {
const handleMessage = (evt: MessageEvent) => {
if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) {
if (
evt.origin === new URL(ssoRedirectURL).origin &&
ssoWindow &&
evt.data === 'authDone' &&
evt.source === ssoWindow
) {
ssoWindow.close();
setSSOWindow(undefined);
handleSubmit();
@ -37,7 +42,7 @@ export function SSOStage({
return () => {
window.removeEventListener('message', handleMessage);
};
}, [ssoWindow, handleSubmit]);
}, [ssoWindow, handleSubmit, ssoRedirectURL]);
return (
<Dialog>

View file

@ -30,7 +30,15 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
const imgUrl = mxcUrlToHttp(
mx,
prev['og:image'] || '',
useAuthentication,
256,
256,
'scale',
false
);
return (
<>
@ -42,7 +50,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
as="a"
href={url}
target="_blank"
rel="no-referrer"
rel="noreferrer"
size="T200"
priority="300"
>

View file

@ -190,6 +190,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = {
name: 'Other',
items: [
{
location: {
state: true,
key: StateEvent.PoniesRoomEmotes,
},
name: 'Manage Emojis & Stickers',
},
{
location: {
state: true,

View file

@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) {
<Box direction="Column" gap="100">
<Box gap="100" alignItems="End">
<Text size="H3">Cinny</Text>
<Text size="T200">v4.10.3</Text>
<Text size="T200">v4.10.5</Text>
</Box>
<Text>Yet another matrix client.</Text>
</Box>

View file

@ -125,6 +125,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = {
name: 'Other',
items: [
{
location: {
state: true,
key: StateEvent.PoniesRoomEmotes,
},
name: 'Manage Emojis & Stickers',
},
{
location: {
state: true,

View file

@ -18,7 +18,7 @@ export const useSelectedSpace = (): string | undefined => {
export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({
path: getSpaceLobbyPath(spaceIdOrAlias),
path: decodeURIComponent(getSpaceLobbyPath(spaceIdOrAlias)),
caseSensitive: true,
end: false,
});
@ -28,7 +28,7 @@ export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
export const useSpaceSearchSelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({
path: getSpaceSearchPath(spaceIdOrAlias),
path: decodeURIComponent(getSpaceSearchPath(spaceIdOrAlias)),
caseSensitive: true,
end: false,
});

View file

@ -72,7 +72,6 @@ import { pushSessionToSW } from '../../sw-session';
import { CallProvider } from './client/call/CallProvider';
import { PersistentCallContainer } from './client/call/PersistentCallContainer';
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
const { hashRouter } = clientConfig;
const mobile = screenSize === ScreenSize.Mobile;
@ -119,7 +118,6 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
return redirect(getLoginPath());
}
pushSessionToSW(session.baseUrl, session.accessToken);
return null;
}}
element={

View file

@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank"
rel="noreferrer"
>
v4.10.3
v4.10.5
</Text>
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Twitter

View file

@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank"
rel="noreferrer noopener"
>
v4.10.3
v4.10.5
</a>
</span>
}

View file

@ -34,17 +34,14 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
navigator.serviceWorker.ready.then(sendSessionToSW);
window.addEventListener('load', sendSessionToSW);
// When returning from background
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
navigator.serviceWorker.addEventListener('message', (ev) => {
const { type } = ev.data ?? {};
if (type === 'requestSession') {
sendSessionToSW();
}
});
// When restored from bfcache (important on iOS)
window.addEventListener('pageshow', sendSessionToSW);
}
const mountApp = () => {

128
src/sw.ts
View file

@ -3,14 +3,6 @@
export type {};
declare const self: ServiceWorkerGlobalScope;
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(self.clients.claim());
});
type SessionInfo = {
accessToken: string;
baseUrl: string;
@ -21,6 +13,9 @@ type SessionInfo = {
*/
const sessions = new Map<string, SessionInfo>();
const clientToResolve = new Map<string, (value: SessionInfo | undefined) => void>();
const clientToSessionPromise = new Map<string, Promise<SessionInfo | undefined>>();
async function cleanupDeadClients() {
const activeClients = await self.clients.matchAll();
const activeIds = new Set(activeClients.map((c) => c.id));
@ -28,10 +23,72 @@ async function cleanupDeadClients() {
Array.from(sessions.keys()).forEach((id) => {
if (!activeIds.has(id)) {
sessions.delete(id);
clientToResolve.delete(id);
clientToSessionPromise.delete(id);
}
});
}
function setSession(clientId: string, accessToken: any, baseUrl: any) {
if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
sessions.set(clientId, { accessToken, baseUrl });
} else {
// Logout or invalid session
sessions.delete(clientId);
}
const resolveSession = clientToResolve.get(clientId);
if (resolveSession) {
resolveSession(sessions.get(clientId));
clientToResolve.delete(clientId);
clientToSessionPromise.delete(clientId);
}
}
function requestSession(client: Client): Promise<SessionInfo | undefined> {
const promise =
clientToSessionPromise.get(client.id) ??
new Promise((resolve) => {
clientToResolve.set(client.id, resolve);
client.postMessage({ type: 'requestSession' });
});
if (!clientToSessionPromise.has(client.id)) {
clientToSessionPromise.set(client.id, promise);
}
return promise;
}
async function requestSessionWithTimeout(
clientId: string,
timeoutMs = 3000
): Promise<SessionInfo | undefined> {
const client = await self.clients.get(clientId);
if (!client) return undefined;
const sessionPromise = requestSession(client);
const timeout = new Promise<undefined>((resolve) => {
setTimeout(() => resolve(undefined), timeoutMs);
});
return Promise.race([sessionPromise, timeout]);
}
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
(async () => {
await self.clients.claim();
await cleanupDeadClients();
})()
);
});
/**
* Receive session updates from clients
*/
@ -41,23 +98,28 @@ self.addEventListener('message', (event: ExtendableMessageEvent) => {
const { type, accessToken, baseUrl } = event.data || {};
if (type !== 'setSession') return;
cleanupDeadClients();
if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
sessions.set(client.id, { accessToken, baseUrl });
} else {
// Logout or invalid session
sessions.delete(client.id);
if (type === 'setSession') {
setSession(client.id, accessToken, baseUrl);
cleanupDeadClients();
}
});
function validMediaRequest(url: string, baseUrl: string): boolean {
const downloadUrl = new URL('/_matrix/client/v1/media/download', baseUrl);
const thumbnailUrl = new URL('/_matrix/client/v1/media/thumbnail', baseUrl);
const MEDIA_PATHS = ['/_matrix/client/v1/media/download', '/_matrix/client/v1/media/thumbnail'];
return url.startsWith(downloadUrl.href) || url.startsWith(thumbnailUrl.href);
function mediaPath(url: string): boolean {
try {
const { pathname } = new URL(url);
return MEDIA_PATHS.some((p) => pathname.startsWith(p));
} catch {
return false;
}
}
function validMediaRequest(url: string, baseUrl: string): boolean {
return MEDIA_PATHS.some((p) => {
const validUrl = new URL(p, baseUrl);
return url.startsWith(validUrl.href);
});
}
function fetchConfig(token: string): RequestInit {
@ -72,13 +134,25 @@ function fetchConfig(token: string): RequestInit {
self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request;
if (method !== 'GET') return;
if (!event.clientId) return;
if (method !== 'GET' || !mediaPath(url)) return;
const session = sessions.get(event.clientId);
if (!session) return;
const { clientId } = event;
if (!clientId) return;
if (!validMediaRequest(url, session.baseUrl)) return;
const session = sessions.get(clientId);
if (session) {
if (validMediaRequest(url, session.baseUrl)) {
event.respondWith(fetch(url, fetchConfig(session.accessToken)));
}
return;
}
event.respondWith(fetch(url, fetchConfig(session.accessToken)));
event.respondWith(
requestSessionWithTimeout(clientId).then((s) => {
if (s && validMediaRequest(url, s.baseUrl)) {
return fetch(url, fetchConfig(s.accessToken));
}
return fetch(event.request);
})
);
});