Compare commits

..

30 commits

Author SHA1 Message Date
2dc3dfa38d
chore: change settings for supnas 2024-11-29 21:56:29 +09:00
3c4b7e5c33
Merge upstream tag 'v4.2.3' 2024-11-29 21:55:26 +09:00
Krishan
a142630ff9
Release v4.2.3 (#2052) 2024-11-12 20:45:34 +11:00
renovate[bot]
492a149c7f
fix(deps): update dependency matrix-js-sdk to v34.11.1 (#2053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 20:43:50 +11:00
Krishan
c110e64341
Release v4.2.2 (#2012) 2024-10-16 21:29:30 +11:00
夜坂雅
0e51e19cab
fix: register service worker immediately and cache media requests (#1977)
* Allow service worker to immediately claim pages
* Allow media requests to be cached by browser
2024-10-16 21:26:03 +11:00
renovate[bot]
35b0b1ea42
fix(deps): update dependency matrix-js-sdk to v34.8.0 (#2011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 21:22:06 +11:00
dependabot[bot]
cca8b5f2b2
Bump actions/setup-node from 4.0.3 to 4.0.4 (#1969)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-03 17:03:33 +10:00
dependabot[bot]
48265c4227
Bump actions/checkout from 4.1.7 to 4.2.0 (#1985)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0.
- [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.1.7...v4.2.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  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>
2024-10-03 17:00:32 +10:00
dependabot[bot]
c38efdfbce
Bump docker/build-push-action from 6.7.0 to 6.9.0 (#1986)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  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>
2024-10-03 16:59:55 +10:00
dependabot[bot]
d8833a310d
Bump cla-assistant/github-action from 2.5.1 to 2.6.1 (#1987)
Bumps [cla-assistant/github-action](https://github.com/cla-assistant/github-action) from 2.5.1 to 2.6.1.
- [Release notes](https://github.com/cla-assistant/github-action/releases)
- [Commits](https://github.com/cla-assistant/github-action/compare/v2.5.1...v2.6.1)

---
updated-dependencies:
- dependency-name: cla-assistant/github-action
  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>
2024-10-03 16:59:12 +10:00
Ajay Bura
5824d7c716
fix font-weight in dark theme with unsupported fonts (#1964) 2024-09-22 22:31:32 +10:00
Krishan
6e191d3c79
Fix matrix.to links opening in webview in cinny desktop (#1963) 2024-09-22 10:08:55 +05:30
Krishan
21164a9b61
Release v4.2.1 (#1953) 2024-09-14 23:24:34 +10:00
Krishan
4923b17ad6
Fix auth media check for dendrite (#1952) 2024-09-14 18:54:06 +05:30
Krishan
c75e903619
Release v4.2.0 (#1949) 2024-09-11 19:26:08 +05:30
renovate[bot]
042cbc4453
Update dependency matrix-js-sdk to v34.5.0 (#1945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 17:08:38 +10:00
Ajay Bura
03cc25eec0
Fix authenticated media download (#1947)
* remove dead function

* fix media download in room timeline

* authenticate remaining media endpoints
2024-09-11 17:07:02 +10:00
夜坂雅
f2c31d29a2
fix: Fix video and audio loading with authenicated media (#1946)
Appeareantly Firefox (and maybe Chrome) won't let service workers take over requests from <video> and <audio> tags, so we just fetch the URL ourselves.
2024-09-11 10:43:15 +05:30
Ajay Bura
5482f8e72e
render matrix room and event link content (#1938) 2024-09-09 20:51:52 +10:00
Ajay Bura
96df140153
Improve-auth-media (#1933)
* fix set power level broken after sdk update

* add media authentication hook

* fix service worker types

* fix service worker not working in dev mode

* fix env mode check when registering sw
2024-09-09 14:15:20 +05:30
Ajay Bura
4dfce32730
fix mention url is encoded wrong (#1936) 2024-09-08 22:53:59 +10:00
Ajay Bura
388f606ad2
fix escape to mark as read (#1935) 2024-09-08 22:53:17 +10:00
Ajay Bura
09444f9e08
fix sso login without identity providers (#1934) 2024-09-08 22:51:43 +10:00
夜坂雅
c6a8fb1117
Add authenticated media support (#1930)
* chore: Bump matrix-js-sdk to 34.4.0

* feat: Authenticated media support

* chore: Use Vite PWA for service worker support

* fix: Fix Vite PWA SW entry point

Forget this. :P

* fix: Also add Nginx rewrite for sw.js

* fix: Correct Nginx rewrite

* fix: Add Netlify redirect for sw.js

Otherwise the generic SPA rewrite to index.html would take effect, breaking Service Worker.

* fix: Account for subpath when regisering service worker

* chore: Correct types
2024-09-07 19:15:55 +05:30
Dylan Hackworth
043012e809
pressing up to edit should take you to end of line (#1928) 2024-09-07 18:38:16 +05:30
utf
5c9ee1a988
Fix IPv6 support for the Docker container (#1884)
* Fix `docker-nginx.conf` indentation

* Listen on IPv4 and IPv6 inside Docker
2024-08-23 20:56:03 +10:00
Krishan
22b7f6dd7d
Create Code of Conduct (#1908) 2024-08-21 15:43:40 +05:30
dependabot[bot]
bdba0332e1
Bump cla-assistant/github-action from 2.4.0 to 2.5.1 (#1905)
Bumps [cla-assistant/github-action](https://github.com/cla-assistant/github-action) from 2.4.0 to 2.5.1.
- [Release notes](https://github.com/cla-assistant/github-action/releases)
- [Commits](https://github.com/cla-assistant/github-action/compare/v2.4.0...v2.5.1)

---
updated-dependencies:
- dependency-name: cla-assistant/github-action
  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>
2024-08-21 00:22:26 +10:00
dependabot[bot]
16be69c104
Bump docker/build-push-action from 6.6.1 to 6.7.0 (#1906)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.1 to 6.7.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.6.1...v6.7.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  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>
2024-08-21 00:21:25 +10:00
72 changed files with 4034 additions and 735 deletions

View file

@ -12,9 +12,9 @@ jobs:
PR_NUMBER: ${{github.event.number}} PR_NUMBER: ${{github.event.number}}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.0
- name: Setup node - name: Setup node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version: 20.12.2 node-version: 20.12.2
cache: 'npm' cache: 'npm'

View file

@ -12,7 +12,7 @@ jobs:
- name: 'CLA Assistant' - name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release # Beta Release
uses: cla-assistant/github-action@v2.4.0 uses: cla-assistant/github-action@v2.6.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret # the below token should have repo scope and must be manually added by you in the repository's secret

View file

@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.0
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v6.9.0
with: with:
context: . context: .
push: false push: false

View file

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

View file

@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.0
- name: Setup node - name: Setup node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version: 20.12.2 node-version: 20.12.2
cache: 'npm' cache: 'npm'

View file

@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.0
- name: Setup node - name: Setup node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version: 20.12.2 node-version: 20.12.2
cache: 'npm' cache: 'npm'
@ -66,7 +66,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
@ -90,7 +90,7 @@ jobs:
${{ secrets.DOCKER_USERNAME }}/cinny ${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v6.9.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

3
.npmrc
View file

@ -1,3 +1,2 @@
legacy-peer-deps=true legacy-peer-deps=true
save-exact=true save-exact=true
@matrix-org:registry=https://gitlab.matrix.org/api/v4/projects/27/packages/npm/

128
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
cinnyapp@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -24,6 +24,7 @@ server {
rewrite ^/manifest.json$ /manifest.json break; rewrite ^/manifest.json$ /manifest.json break;
rewrite ^.*/olm.wasm$ /olm.wasm break; rewrite ^.*/olm.wasm$ /olm.wasm break;
rewrite ^/sw.js$ /sw.js break;
rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break; rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break;
rewrite ^/public/(.*)$ /public/$1 break; rewrite ^/public/(.*)$ /public/$1 break;

View file

@ -1,16 +1,20 @@
server { server {
location / { listen 80;
root /usr/share/nginx/html; listen [::]:80;
rewrite ^/config.json$ /config.json break; location / {
root /usr/share/nginx/html;
rewrite ^/config.json$ /config.json break;
rewrite ^/manifest.json$ /manifest.json break; rewrite ^/manifest.json$ /manifest.json break;
rewrite ^.*/olm.wasm$ /olm.wasm break; rewrite ^.*/olm.wasm$ /olm.wasm break;
rewrite ^/sw.js$ /sw.js break;
rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break; rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break;
rewrite ^/public/(.*)$ /public/$1 break; rewrite ^/public/(.*)$ /public/$1 break;
rewrite ^/assets/(.*)$ /assets/$1 break; rewrite ^/assets/(.*)$ /assets/$1 break;
rewrite ^(.+)$ /index.html break; rewrite ^(.+)$ /index.html break;
} }
} }

View file

@ -7,7 +7,12 @@
from = "/manifest.json" from = "/manifest.json"
to = "/manifest.json" to = "/manifest.json"
status = 200 status = 200
[[redirects]]
from = "/sw.js"
to = "/sw.js"
status = 200
[[redirects]] [[redirects]]
from = "*/olm.wasm" from = "*/olm.wasm"
to = "/olm.wasm" to = "/olm.wasm"

3358
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.1.0", "version": "4.2.3",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@ -24,7 +24,7 @@
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
"@fontsource/inter": "4.5.14", "@fontsource/inter": "4.5.14",
"@matrix-org/olm": "3.2.14", "@matrix-org/olm": "3.2.15",
"@tanstack/react-query": "5.24.1", "@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0", "@tanstack/react-virtual": "3.2.0",
@ -56,7 +56,7 @@
"jotai": "2.6.0", "jotai": "2.6.0",
"linkify-react": "4.1.3", "linkify-react": "4.1.3",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.3",
"matrix-js-sdk": "29.1.0", "matrix-js-sdk": "34.11.1",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
"prismjs": "1.29.0", "prismjs": "1.29.0",
@ -106,6 +106,7 @@
"sass": "1.56.2", "sass": "1.56.2",
"typescript": "4.9.4", "typescript": "4.9.4",
"vite": "5.0.13", "vite": "5.0.13",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4", "vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.1" "vite-plugin-top-level-await": "1.4.1"
} }

View file

@ -14,6 +14,8 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getEmojiUrl, isUsingTwemoji } from '../../plugins/emoji'; import { getEmojiUrl, isUsingTwemoji } from '../../plugins/emoji';
import { getBeginCommand } from './utils'; import { getBeginCommand } from './utils';
import { BlockType } from './types'; import { BlockType } from './types';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
// Put this at the start and end of an inline component to work around this Chromium bug: // Put this at the start and end of an inline component to work around this Chromium bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405 // https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
@ -77,6 +79,7 @@ function RenderEmoticonElement({
children, children,
}: { element: EmoticonElement } & RenderElementProps) { }: { element: EmoticonElement } & RenderElementProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const selected = useSelected(); const selected = useSelected();
const focused = useFocused(); const focused = useFocused();
@ -91,7 +94,7 @@ function RenderEmoticonElement({
{element.key.startsWith('mxc://') ? ( {element.key.startsWith('mxc://') ? (
<img <img
className={css.EmoticonImg} className={css.EmoticonImg}
src={mx.mxcUrlToHttp(element.key) ?? element.key} src={mxcUrlToHttp(mx, element.key, useAuthentication) ?? element.key}
alt={element.shortcode} alt={element.shortcode}
/> />
) : ( ) : (

View file

@ -18,6 +18,8 @@ import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
import { IEmoji, emojis } from '../../../plugins/emoji'; import { IEmoji, emojis } from '../../../plugins/emoji';
import { ExtendedPackImage, PackUsage } from '../../../plugins/custom-emoji'; import { ExtendedPackImage, PackUsage } from '../../../plugins/custom-emoji';
import { useKeyDown } from '../../../hooks/useKeyDown'; import { useKeyDown } from '../../../hooks/useKeyDown';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type EmoticonCompleteHandler = (key: string, shortcode: string) => void; type EmoticonCompleteHandler = (key: string, shortcode: string) => void;
@ -48,6 +50,7 @@ export function EmoticonAutocomplete({
requestClose, requestClose,
}: EmoticonAutocompleteProps) { }: EmoticonAutocompleteProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const imagePacks = useRelevantImagePacks(mx, PackUsage.Emoticon, imagePackRooms); const imagePacks = useRelevantImagePacks(mx, PackUsage.Emoticon, imagePackRooms);
const recentEmoji = useRecentEmoji(mx, 20); const recentEmoji = useRecentEmoji(mx, 20);
@ -103,7 +106,7 @@ export function EmoticonAutocomplete({
<Box <Box
shrink="No" shrink="No"
as="img" as="img"
src={mx.mxcUrlToHttp(key) || key} src={mxcUrlToHttp(mx, key, useAuthentication) || key}
alt={emoticon.shortcode} alt={emoticon.shortcode}
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }} style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
/> />

View file

@ -18,6 +18,7 @@ import { useKeyDown } from '../../../hooks/useKeyDown';
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix'; import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room'; import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
import { UserAvatar } from '../../user-avatar'; import { UserAvatar } from '../../user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type MentionAutoCompleteHandler = (userId: string, name: string) => void; type MentionAutoCompleteHandler = (userId: string, name: string) => void;
@ -84,6 +85,7 @@ export function UserMentionAutocomplete({
requestClose, requestClose,
}: UserMentionAutocompleteProps) { }: UserMentionAutocompleteProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const roomId: string = room.roomId!; const roomId: string = room.roomId!;
const roomAliasOrId = room.getCanonicalAlias() || roomId; const roomAliasOrId = room.getCanonicalAlias() || roomId;
const members = useRoomMembers(mx, roomId); const members = useRoomMembers(mx, roomId);
@ -143,7 +145,10 @@ export function UserMentionAutocomplete({
/> />
) : ( ) : (
autoCompleteMembers.map((roomMember) => { autoCompleteMembers.map((roomMember) => {
const avatarUrl = roomMember.getAvatarUrl(mx.baseUrl, 32, 32, 'crop', undefined, false); const avatarMxcUrl = roomMember.getMxcAvatarUrl();
const avatarUrl = avatarMxcUrl
? mx.mxcUrlToHttp(avatarMxcUrl, 32, 32, 'crop', undefined, false, useAuthentication)
: undefined;
return ( return (
<MenuItem <MenuItem
key={roomMember.userId} key={roomMember.userId}

View file

@ -62,7 +62,7 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
} }
const matrixTo = `https://matrix.to/#/${fragment}`; const matrixTo = `https://matrix.to/#/${fragment}`;
return `<a href="${encodeURIComponent(matrixTo)}">${sanitizeText(node.name)}</a>`; return `<a href="${encodeURI(matrixTo)}">${sanitizeText(node.name)}</a>`;
} }
case BlockType.Emoticon: case BlockType.Emoticon:
return node.key.startsWith('mxc://') return node.key.startsWith('mxc://')
@ -71,7 +71,7 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
)}" title="${sanitizeText(node.shortcode)}" height="32" />` )}" title="${sanitizeText(node.shortcode)}" height="32" />`
: sanitizeText(node.key); : sanitizeText(node.key);
case BlockType.Link: case BlockType.Link:
return `<a href="${encodeURIComponent(node.href)}">${node.children}</a>`; return `<a href="${encodeURI(node.href)}">${node.children}</a>`;
case BlockType.Command: case BlockType.Command:
return `/${sanitizeText(node.command)}`; return `/${sanitizeText(node.command)}`;
default: default:

View file

@ -50,13 +50,14 @@ import { useRelevantImagePacks } from '../../hooks/useImagePacks';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../hooks/useRecentEmoji';
import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji'; import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji';
import { isUserId } from '../../utils/matrix'; import { isUserId, mxcUrlToHttp } from '../../utils/matrix';
import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom'; import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce'; import { useDebounce } from '../../hooks/useDebounce';
import { useThrottle } from '../../hooks/useThrottle'; import { useThrottle } from '../../hooks/useThrottle';
import { addRecentEmoji } from '../../plugins/recent-emoji'; import { addRecentEmoji } from '../../plugins/recent-emoji';
import { mobileOrTablet } from '../../utils/user-agent'; import { mobileOrTablet } from '../../utils/user-agent';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const RECENT_GROUP_ID = 'recent_group'; const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group'; const SEARCH_GROUP_ID = 'search_group';
@ -371,11 +372,13 @@ function ImagePackSidebarStack({
packs, packs,
usage, usage,
onItemClick, onItemClick,
useAuthentication,
}: { }: {
mx: MatrixClient; mx: MatrixClient;
packs: ImagePack[]; packs: ImagePack[];
usage: PackUsage; usage: PackUsage;
onItemClick: (id: string) => void; onItemClick: (id: string) => void;
useAuthentication?: boolean;
}) { }) {
const activeGroupId = useAtomValue(activeGroupIdAtom); const activeGroupId = useAtomValue(activeGroupIdAtom);
return ( return (
@ -398,7 +401,7 @@ function ImagePackSidebarStack({
height: toRem(24), height: toRem(24),
objectFit: 'contain', objectFit: 'contain',
}} }}
src={mx.mxcUrlToHttp(pack.getPackAvatarUrl(usage) ?? '') || pack.avatarUrl} src={mxcUrlToHttp(mx, pack.getPackAvatarUrl(usage) ?? '', useAuthentication) || pack.avatarUrl}
alt={label || 'Unknown Pack'} alt={label || 'Unknown Pack'}
/> />
</SidebarBtn> </SidebarBtn>
@ -470,68 +473,70 @@ export function SearchEmojiGroup({
label, label,
id, id,
emojis: searchResult, emojis: searchResult,
useAuthentication,
}: { }: {
mx: MatrixClient; mx: MatrixClient;
tab: EmojiBoardTab; tab: EmojiBoardTab;
label: string; label: string;
id: string; id: string;
emojis: Array<ExtendedPackImage | IEmoji>; emojis: Array<ExtendedPackImage | IEmoji>;
useAuthentication?: boolean;
}) { }) {
return ( return (
<EmojiGroup key={id} id={id} label={label}> <EmojiGroup key={id} id={id} label={label}>
{tab === EmojiBoardTab.Emoji {tab === EmojiBoardTab.Emoji
? searchResult.map((emoji) => ? searchResult.map((emoji) =>
'unicode' in emoji ? ( 'unicode' in emoji ? (
<EmojiItem <EmojiItem
key={emoji.unicode} key={emoji.unicode}
label={emoji.label} label={emoji.label}
type={EmojiType.Emoji} type={EmojiType.Emoji}
data={emoji.unicode} data={emoji.unicode}
shortcode={emoji.shortcode} shortcode={emoji.shortcode}
> >
{NativeEmoji(emoji)} {NativeEmoji(emoji)}
</EmojiItem> </EmojiItem>
) : ( ) : (
<EmojiItem <EmojiItem
key={emoji.shortcode} key={emoji.shortcode}
label={emoji.body || emoji.shortcode} label={emoji.body || emoji.shortcode}
type={EmojiType.CustomEmoji} type={EmojiType.CustomEmoji}
data={emoji.url} data={emoji.url}
shortcode={emoji.shortcode} shortcode={emoji.shortcode}
> >
<img <img
loading="lazy" loading="lazy"
className={css.CustomEmojiImg} className={css.CustomEmojiImg}
alt={emoji.body || emoji.shortcode} alt={emoji.body || emoji.shortcode}
src={mx.mxcUrlToHttp(emoji.url) ?? emoji.url} src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/> />
</EmojiItem> </EmojiItem>
)
) )
)
: searchResult.map((emoji) => : searchResult.map((emoji) =>
'unicode' in emoji ? null : ( 'unicode' in emoji ? null : (
<StickerItem <StickerItem
key={emoji.shortcode} key={emoji.shortcode}
label={emoji.body || emoji.shortcode} label={emoji.body || emoji.shortcode}
type={EmojiType.Sticker} type={EmojiType.Sticker}
data={emoji.url} data={emoji.url}
shortcode={emoji.shortcode} shortcode={emoji.shortcode}
> >
<img <img
loading="lazy" loading="lazy"
className={css.StickerImg} className={css.StickerImg}
alt={emoji.body || emoji.shortcode} alt={emoji.body || emoji.shortcode}
src={mx.mxcUrlToHttp(emoji.url) ?? emoji.url} src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/> />
</StickerItem> </StickerItem>
) )
)} )}
</EmojiGroup> </EmojiGroup>
); );
} }
export const CustomEmojiGroups = memo( export const CustomEmojiGroups = memo(
({ mx, groups }: { mx: MatrixClient; groups: ImagePack[] }) => ( ({ mx, groups, useAuthentication }: { mx: MatrixClient; groups: ImagePack[]; useAuthentication?: boolean }) => (
<> <>
{groups.map((pack) => ( {groups.map((pack) => (
<EmojiGroup key={pack.id} id={pack.id} label={pack.displayName || 'Unknown'}> <EmojiGroup key={pack.id} id={pack.id} label={pack.displayName || 'Unknown'}>
@ -547,7 +552,7 @@ export const CustomEmojiGroups = memo(
loading="lazy" loading="lazy"
className={css.CustomEmojiImg} className={css.CustomEmojiImg}
alt={image.body || image.shortcode} alt={image.body || image.shortcode}
src={mx.mxcUrlToHttp(image.url) ?? image.url} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/> />
</EmojiItem> </EmojiItem>
))} ))}
@ -557,7 +562,7 @@ export const CustomEmojiGroups = memo(
) )
); );
export const StickerGroups = memo(({ mx, groups }: { mx: MatrixClient; groups: ImagePack[] }) => ( export const StickerGroups = memo(({ mx, groups, useAuthentication }: { mx: MatrixClient; groups: ImagePack[]; useAuthentication?: boolean }) => (
<> <>
{groups.length === 0 && ( {groups.length === 0 && (
<Box <Box
@ -590,7 +595,7 @@ export const StickerGroups = memo(({ mx, groups }: { mx: MatrixClient; groups: I
loading="lazy" loading="lazy"
className={css.StickerImg} className={css.StickerImg}
alt={image.body || image.shortcode} alt={image.body || image.shortcode}
src={mx.mxcUrlToHttp(image.url) ?? image.url} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/> />
</StickerItem> </StickerItem>
))} ))}
@ -662,6 +667,7 @@ export function EmojiBoard({
const setActiveGroupId = useSetAtom(activeGroupIdAtom); const setActiveGroupId = useSetAtom(activeGroupIdAtom);
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const emojiGroupLabels = useEmojiGroupLabels(); const emojiGroupLabels = useEmojiGroupLabels();
const emojiGroupIcons = useEmojiGroupIcons(); const emojiGroupIcons = useEmojiGroupIcons();
const imagePacks = useRelevantImagePacks(mx, usage, imagePackRooms); const imagePacks = useRelevantImagePacks(mx, usage, imagePackRooms);
@ -755,14 +761,14 @@ export function EmojiBoard({
} else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) { } else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = css.CustomEmojiImg; img.className = css.CustomEmojiImg;
img.setAttribute('src', mx.mxcUrlToHttp(emojiInfo.data) || emojiInfo.data); img.setAttribute('src', mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data);
img.setAttribute('alt', emojiInfo.shortcode); img.setAttribute('alt', emojiInfo.shortcode);
emojiPreviewRef.current.textContent = ''; emojiPreviewRef.current.textContent = '';
emojiPreviewRef.current.appendChild(img); emojiPreviewRef.current.appendChild(img);
} }
emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`; emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`;
}, },
[mx] [mx, useAuthentication]
); );
const throttleEmojiHover = useThrottle(handleEmojiPreview, { const throttleEmojiHover = useThrottle(handleEmojiPreview, {
@ -855,6 +861,7 @@ export function EmojiBoard({
usage={usage} usage={usage}
packs={imagePacks} packs={imagePacks}
onItemClick={handleScrollToGroup} onItemClick={handleScrollToGroup}
useAuthentication={useAuthentication}
/> />
)} )}
{emojiTab && ( {emojiTab && (
@ -916,13 +923,14 @@ export function EmojiBoard({
id={SEARCH_GROUP_ID} id={SEARCH_GROUP_ID}
label={result.items.length ? 'Search Results' : 'No Results found'} label={result.items.length ? 'Search Results' : 'No Results found'}
emojis={result.items} emojis={result.items}
useAuthentication={useAuthentication}
/> />
)} )}
{emojiTab && recentEmojis.length > 0 && ( {emojiTab && recentEmojis.length > 0 && (
<RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} /> <RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} />
)} )}
{emojiTab && <CustomEmojiGroups mx={mx} groups={imagePacks} />} {emojiTab && <CustomEmojiGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />}
{stickerTab && <StickerGroups mx={mx} groups={imagePacks} />} {stickerTab && <StickerGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />}
{emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />} {emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />}
</Box> </Box>
</Scroll> </Scroll>

View file

@ -21,6 +21,7 @@ import * as css from './EventReaders.css';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { openProfileViewer } from '../../../client/action/navigation'; import { openProfileViewer } from '../../../client/action/navigation';
import { UserAvatar } from '../user-avatar'; import { UserAvatar } from '../user-avatar';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
export type EventReadersProps = { export type EventReadersProps = {
room: Room; room: Room;
@ -30,6 +31,7 @@ export type EventReadersProps = {
export const EventReaders = as<'div', EventReadersProps>( export const EventReaders = as<'div', EventReadersProps>(
({ className, room, eventId, requestClose, ...props }, ref) => { ({ className, room, eventId, requestClose, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const latestEventReaders = useRoomEventReaders(room, eventId); const latestEventReaders = useRoomEventReaders(room, eventId);
const getName = (userId: string) => const getName = (userId: string) =>
@ -55,9 +57,10 @@ export const EventReaders = as<'div', EventReadersProps>(
<Box className={css.Content} direction="Column"> <Box className={css.Content} direction="Column">
{latestEventReaders.map((readerId) => { {latestEventReaders.map((readerId) => {
const name = getName(readerId); const name = getName(readerId);
const avatarUrl = room const avatarMxcUrl = room
.getMember(readerId) .getMember(readerId)
?.getAvatarUrl(mx.baseUrl, 100, 100, 'crop', undefined, false); ?.getMxcAvatarUrl();
const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) : undefined;
return ( return (
<MenuItem <MenuItem

View file

@ -6,6 +6,7 @@ import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
import * as css from './ImageViewer.css'; import * as css from './ImageViewer.css';
import { useZoom } from '../../hooks/useZoom'; import { useZoom } from '../../hooks/useZoom';
import { usePan } from '../../hooks/usePan'; import { usePan } from '../../hooks/usePan';
import { downloadMedia } from '../../utils/matrix';
export type ImageViewerProps = { export type ImageViewerProps = {
alt: string; alt: string;
@ -18,8 +19,9 @@ export const ImageViewer = as<'div', ImageViewerProps>(
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2); const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);
const { pan, cursor, onMouseDown } = usePan(zoom !== 1); const { pan, cursor, onMouseDown } = usePan(zoom !== 1);
const handleDownload = () => { const handleDownload = async () => {
FileSaver.saveAs(src, alt); const fileContent = await downloadMedia(src);
FileSaver.saveAs(fileContent, alt);
}; };
return ( return (

View file

@ -5,7 +5,7 @@ import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
import * as css from './Reaction.css'; import * as css from './Reaction.css';
import { getHexcodeForEmoji, getShortcodeFor, getEmojiUrl, isUsingTwemoji } from '../../plugins/emoji'; import { getHexcodeForEmoji, getShortcodeFor, getEmojiUrl, isUsingTwemoji } from '../../plugins/emoji';
import { getMemberDisplayName } from '../../utils/room'; import { getMemberDisplayName } from '../../utils/room';
import { eventWithShortcode, getMxIdLocalPart } from '../../utils/matrix'; import { eventWithShortcode, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
export const Reaction = as< export const Reaction = as<
'button', 'button',
@ -13,8 +13,9 @@ export const Reaction = as<
mx: MatrixClient; mx: MatrixClient;
count: number; count: number;
reaction: string; reaction: string;
useAuthentication?: boolean;
} }
>(({ className, mx, count, reaction, ...props }, ref) => ( >(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => (
<Box <Box
as="button" as="button"
className={classNames(css.Reaction, className)} className={classNames(css.Reaction, className)}
@ -28,7 +29,8 @@ export const Reaction = as<
{reaction.startsWith('mxc://') ? ( {reaction.startsWith('mxc://') ? (
<img <img
className={css.ReactionImg} className={css.ReactionImg}
src={mx.mxcUrlToHttp(reaction) ?? reaction} src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
}
alt={reaction} alt={reaction}
/> />
) : isUsingTwemoji() ? ( ) : isUsingTwemoji() ? (

View file

@ -5,7 +5,6 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { Range } from 'react-range'; import { Range } from 'react-range';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util';
import { IAudioInfo } from '../../../../types/matrix/common'; import { IAudioInfo } from '../../../../types/matrix/common';
import { import {
PlayTimeCallback, PlayTimeCallback,
@ -17,6 +16,13 @@ import {
} from '../../../hooks/media'; } from '../../../hooks/media';
import { useThrottle } from '../../../hooks/useThrottle'; import { useThrottle } from '../../../hooks/useThrottle';
import { secondsToMinutesAndSeconds } from '../../../utils/common'; import { secondsToMinutesAndSeconds } from '../../../utils/common';
import {
decryptFile,
downloadEncryptedMedia,
downloadMedia,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
const PLAY_TIME_THROTTLE_OPS = { const PLAY_TIME_THROTTLE_OPS = {
wait: 500, wait: 500,
@ -44,12 +50,16 @@ export function AudioContent({
renderMediaControl, renderMediaControl,
}: AudioContentProps) { }: AudioContentProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(async () => {
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo), const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
[mx, url, mimeType, encInfo] const fileContent = encInfo
) ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
return URL.createObjectURL(fileContent);
}, [mx, url, useAuthentication, mimeType, encInfo])
); );
const audioRef = useRef<HTMLAudioElement | null>(null); const audioRef = useRef<HTMLAudioElement | null>(null);

View file

@ -20,7 +20,6 @@ import FocusTrap from 'focus-trap-react';
import { IFileInfo } from '../../../../types/matrix/common'; import { IFileInfo } from '../../../../types/matrix/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getFileSrcUrl, getSrcFile } from './util';
import { bytesToSize } from '../../../utils/common'; import { bytesToSize } from '../../../utils/common';
import { import {
READABLE_EXT_TO_MIME_TYPE, READABLE_EXT_TO_MIME_TYPE,
@ -30,6 +29,13 @@ import {
} from '../../../utils/mimeTypes'; } from '../../../utils/mimeTypes';
import * as css from './style.css'; import * as css from './style.css';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import {
decryptFile,
downloadEncryptedMedia,
downloadMedia,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
const renderErrorButton = (retry: () => void, text: string) => ( const renderErrorButton = (retry: () => void, text: string) => (
<TooltipProvider <TooltipProvider
@ -75,21 +81,20 @@ type ReadTextFileProps = {
}; };
export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: ReadTextFileProps) { export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: ReadTextFileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [textViewer, setTextViewer] = useState(false); const [textViewer, setTextViewer] = useState(false);
const loadSrc = useCallback(
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo),
[mx, url, mimeType, encInfo]
);
const [textState, loadText] = useAsyncCallback( const [textState, loadText] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const src = await loadSrc(); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const blob = await getSrcFile(src); const fileContent = encInfo
const text = blob.text(); ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
const text = fileContent.text();
setTextViewer(true); setTextViewer(true);
return text; return text;
}, [loadSrc]) }, [mx, useAuthentication, mimeType, encInfo, url])
); );
return ( return (
@ -166,14 +171,18 @@ export type ReadPdfFileProps = {
}; };
export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: ReadPdfFileProps) { export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: ReadPdfFileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [pdfViewer, setPdfViewer] = useState(false); const [pdfViewer, setPdfViewer] = useState(false);
const [pdfState, loadPdf] = useAsyncCallback( const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const httpUrl = await getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
setPdfViewer(true); setPdfViewer(true);
return httpUrl; return URL.createObjectURL(fileContent);
}, [mx, url, mimeType, encInfo]) }, [mx, url, useAuthentication, mimeType, encInfo])
); );
return ( return (
@ -240,13 +249,19 @@ export type DownloadFileProps = {
}; };
export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFileProps) { export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [downloadState, download] = useAsyncCallback( const [downloadState, download] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const httpUrl = await getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo); const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
FileSaver.saveAs(httpUrl, body); const fileContent = encInfo
return httpUrl; ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
}, [mx, url, mimeType, encInfo, body]) : await downloadMedia(mediaUrl);
const fileURL = URL.createObjectURL(fileContent);
FileSaver.saveAs(fileURL, body);
return fileURL;
}, [mx, url, useAuthentication, mimeType, encInfo, body])
); );
return downloadState.status === AsyncStatus.Error ? ( return downloadState.status === AsyncStatus.Error ? (

View file

@ -22,11 +22,12 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { IImageInfo, MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common'; import { IImageInfo, MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getFileSrcUrl } from './util';
import * as css from './style.css'; import * as css from './style.css';
import { bytesToSize } from '../../../utils/common'; import { bytesToSize } from '../../../utils/common';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes'; import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type RenderViewerProps = { type RenderViewerProps = {
src: string; src: string;
@ -69,6 +70,7 @@ export const ImageContent = as<'div', ImageContentProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const blurHash = info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]; const blurHash = info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
const [load, setLoad] = useState(false); const [load, setLoad] = useState(false);
@ -76,10 +78,16 @@ export const ImageContent = as<'div', ImageContentProps>(
const [viewer, setViewer] = useState(false); const [viewer, setViewer] = useState(false);
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(async () => {
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType || FALLBACK_MIMETYPE, encInfo), const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
[mx, url, mimeType, encInfo] if (encInfo) {
) const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
);
return URL.createObjectURL(fileContent);
}
return mediaUrl;
}, [mx, url, useAuthentication, mimeType, encInfo])
); );
const handleLoad = () => { const handleLoad = () => {

View file

@ -2,7 +2,9 @@ import { ReactNode, useCallback, useEffect } from 'react';
import { IThumbnailContent } from '../../../../types/matrix/common'; import { IThumbnailContent } from '../../../../types/matrix/common';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util'; import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
export type ThumbnailContentProps = { export type ThumbnailContentProps = {
info: IThumbnailContent; info: IThumbnailContent;
@ -10,20 +12,27 @@ export type ThumbnailContentProps = {
}; };
export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) { export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [thumbSrcState, loadThumbSrc] = useAsyncCallback( const [thumbSrcState, loadThumbSrc] = useAsyncCallback(
useCallback(() => { useCallback(async () => {
const thumbInfo = info.thumbnail_info; const thumbInfo = info.thumbnail_info;
const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url; const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url;
const encInfo = info.thumbnail_file;
if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') { if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') {
throw new Error('Failed to load thumbnail'); throw new Error('Failed to load thumbnail');
} }
return getFileSrcUrl(
mx.mxcUrlToHttp(thumbMxcUrl) ?? '', const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
thumbInfo.mimetype, if (encInfo) {
info.thumbnail_file const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
); decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
}, [mx, info]) );
return URL.createObjectURL(fileContent);
}
return mediaUrl;
}, [mx, info, useAuthentication])
); );
useEffect(() => { useEffect(() => {

View file

@ -22,9 +22,15 @@ import {
import * as css from './style.css'; import * as css from './style.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util';
import { bytesToSize } from '../../../../util/common'; import { bytesToSize } from '../../../../util/common';
import { millisecondsToMinutesAndSeconds } from '../../../utils/common'; import { millisecondsToMinutesAndSeconds } from '../../../utils/common';
import {
decryptFile,
downloadEncryptedMedia,
downloadMedia,
mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type RenderVideoProps = { type RenderVideoProps = {
title: string; title: string;
@ -61,16 +67,22 @@ export const VideoContent = as<'div', VideoContentProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const blurHash = info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]; const blurHash = info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
const [load, setLoad] = useState(false); const [load, setLoad] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(async () => {
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo), const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
[mx, url, mimeType, encInfo] const fileContent = encInfo
) ? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType, encInfo)
)
: await downloadMedia(mediaUrl);
return URL.createObjectURL(fileContent);
}, [mx, url, useAuthentication, mimeType, encInfo])
); );
const handleLoad = () => { const handleLoad = () => {

View file

@ -1,23 +0,0 @@
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { decryptFile } from '../../../utils/matrix';
export const getFileSrcUrl = async (
httpUrl: string,
mimeType: string,
encInfo?: EncryptedAttachmentInfo
): Promise<string> => {
if (encInfo) {
if (typeof httpUrl !== 'string') throw new Error('Malformed event');
const encRes = await fetch(httpUrl, { method: 'GET' });
const encData = await encRes.arrayBuffer();
const decryptedBlob = await decryptFile(encData, mimeType, encInfo);
return URL.createObjectURL(decryptedBlob);
}
return httpUrl;
};
export const getSrcFile = async (src: string): Promise<Blob> => {
const res = await fetch(src, { method: 'GET' });
const blob = await res.blob();
return blob;
};

View file

@ -21,7 +21,7 @@ import classNames from 'classnames';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import * as css from './style.css'; import * as css from './style.css';
import { RoomAvatar } from '../room-avatar'; import { RoomAvatar } from '../room-avatar';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { nameInitials } from '../../utils/common'; import { nameInitials } from '../../utils/common';
import { millify } from '../../plugins/millify'; import { millify } from '../../plugins/millify';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
@ -32,6 +32,7 @@ import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { getRoomAvatarUrl, getStateEvent } from '../../utils/room'; import { getRoomAvatarUrl, getStateEvent } from '../../utils/room';
import { useStateEventCallback } from '../../hooks/useStateEventCallback'; import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type GridColumnCount = '1' | '2' | '3'; type GridColumnCount = '1' | '2' | '3';
const getGridColumnCount = (gridWidth: number): GridColumnCount => { const getGridColumnCount = (gridWidth: number): GridColumnCount => {
@ -161,6 +162,7 @@ export const RoomCard = as<'div', RoomCardProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias); const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
const joinedRoom = mx.getRoom(joinedRoomId); const joinedRoom = mx.getRoom(joinedRoomId);
const [topicEvent, setTopicEvent] = useState(() => const [topicEvent, setTopicEvent] = useState(() =>
@ -171,8 +173,8 @@ export const RoomCard = as<'div', RoomCardProps>(
const fallbackTopic = roomIdOrAlias; const fallbackTopic = roomIdOrAlias;
const avatar = joinedRoom const avatar = joinedRoom
? getRoomAvatarUrl(mx, joinedRoom, 96) ? getRoomAvatarUrl(mx, joinedRoom, 96, useAuthentication)
: avatarUrl && mx.mxcUrlToHttp(avatarUrl, 96, 96, 'crop'); : avatarUrl && mxcUrlToHttp(mx, avatarUrl, useAuthentication, 96, 96, 'crop');
const roomName = joinedRoom?.name || name || fallbackName; const roomName = joinedRoom?.name || name || fallbackName;
const roomTopic = const roomTopic =

View file

@ -6,7 +6,7 @@ import { openInviteUser } from '../../../client/action/navigation';
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room'; import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
import { getMemberDisplayName, getStateEvent } from '../../utils/room'; import { getMemberDisplayName, getStateEvent } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { timeDayMonthYear, timeHourMinute } from '../../utils/time'; import { timeDayMonthYear, timeHourMinute } from '../../utils/time';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
@ -14,6 +14,7 @@ import { RoomAvatar } from '../room-avatar';
import { nameInitials } from '../../utils/common'; import { nameInitials } from '../../utils/common';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
export type RoomIntroProps = { export type RoomIntroProps = {
room: Room; room: Room;
@ -21,6 +22,7 @@ export type RoomIntroProps = {
export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => { export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { navigateRoom } = useRoomNavigate(); const { navigateRoom } = useRoomNavigate();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
@ -28,7 +30,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const name = useRoomName(room); const name = useRoomName(room);
const topic = useRoomTopic(room); const topic = useRoomTopic(room);
const avatarHttpUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc) : undefined; const avatarHttpUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication) : undefined;
const createContent = createEvent?.getContent<IRoomCreateContent>(); const createContent = createEvent?.getContent<IRoomCreateContent>();
const ts = createEvent?.getTs(); const ts = createEvent?.getTs();

View file

@ -10,12 +10,15 @@ import {
} from '../../hooks/useIntersectionObserver'; } from '../../hooks/useIntersectionObserver';
import * as css from './UrlPreviewCard.css'; import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom'; import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const linkStyles = { color: color.Success.Main }; const linkStyles = { color: color.Success.Main };
export const UrlPreviewCard = as<'div', { url: string; ts: number }>( export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => { ({ url, ts, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [previewStatus, loadPreview] = useAsyncCallback( const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]) useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
); );
@ -27,7 +30,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null; if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => { const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mx.mxcUrlToHttp(prev['og:image'] || '', 256, 256, 'scale', false); const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
return ( return (
<> <>

View file

@ -33,6 +33,8 @@ import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler'; import { BackRouteHandler } from '../../components/BackRouteHandler';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type LobbyMenuProps = { type LobbyMenuProps = {
roomId: string; roomId: string;
@ -122,6 +124,7 @@ type LobbyHeaderProps = {
}; };
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) { export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const space = useSpace(); const space = useSpace();
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
@ -129,7 +132,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
const name = useRoomName(space); const name = useRoomName(space);
const avatarMxc = useRoomAvatar(space); const avatarMxc = useRoomAvatar(space);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => { const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect()); setMenuAnchor(evt.currentTarget.getBoundingClientRect());

View file

@ -11,15 +11,18 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
import * as css from './LobbyHero.css'; import * as css from './LobbyHero.css';
import { PageHero } from '../../components/page'; import { PageHero } from '../../components/page';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard'; import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
export function LobbyHero() { export function LobbyHero() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const space = useSpace(); const space = useSpace();
const name = useRoomName(space); const name = useRoomName(space);
const topic = useRoomTopic(space); const topic = useRoomTopic(space);
const avatarMxc = useRoomAvatar(space); const avatarMxc = useRoomAvatar(space);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
return ( return (
<PageHero <PageHero

View file

@ -39,6 +39,8 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { ErrorCode } from '../../cs-errorcode'; import { ErrorCode } from '../../cs-errorcode';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { ItemDraggableTarget, useDraggableItem } from './DnD'; import { ItemDraggableTarget, useDraggableItem } from './DnD';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type RoomJoinButtonProps = { type RoomJoinButtonProps = {
roomId: string; roomId: string;
@ -334,6 +336,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { roomId, content } = item; const { roomId, content } = item;
const room = getRoom(roomId); const room = getRoom(roomId);
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
@ -364,7 +367,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
name={localSummary.name} name={localSummary.name}
topic={localSummary.topic} topic={localSummary.topic}
avatarUrl={ avatarUrl={
dm ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96) dm ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
} }
memberCount={localSummary.memberCount} memberCount={localSummary.memberCount}
suggested={content.suggested} suggested={content.suggested}
@ -418,8 +421,8 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
topic={summaryState.data.topic} topic={summaryState.data.topic}
avatarUrl={ avatarUrl={
summaryState.data?.avatar_url summaryState.data?.avatar_url
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ?? ? mxcUrlToHttp(mx, summaryState.data.avatar_url, useAuthentication, 96, 96, 'crop') ??
undefined undefined
: undefined : undefined
} }
memberCount={summaryState.data.num_joined_members} memberCount={summaryState.data.num_joined_members}

View file

@ -35,6 +35,8 @@ import { ErrorCode } from '../../cs-errorcode';
import { useDraggableItem } from './DnD'; import { useDraggableItem } from './DnD';
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation'; import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function SpaceProfileLoading() { function SpaceProfileLoading() {
return ( return (
@ -408,6 +410,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { roomId, content } = item; const { roomId, content } = item;
const space = getRoom(roomId); const space = getRoom(roomId);
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
@ -432,7 +435,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
<SpaceProfile <SpaceProfile
roomId={roomId} roomId={roomId}
name={localSummary.name} name={localSummary.name}
avatarUrl={getRoomAvatarUrl(mx, space, 96)} avatarUrl={getRoomAvatarUrl(mx, space, 96, useAuthentication)}
suggested={content.suggested} suggested={content.suggested}
closed={closed} closed={closed}
categoryId={categoryId} categoryId={categoryId}
@ -469,8 +472,8 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
name={summaryState.data.name || summaryState.data.canonical_alias || roomId} name={summaryState.data.name || summaryState.data.canonical_alias || roomId}
avatarUrl={ avatarUrl={
summaryState.data?.avatar_url summaryState.data?.avatar_url
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ?? ? mxcUrlToHttp(mx, summaryState.data.avatar_url, useAuthentication, 96, 96, 'crop') ??
undefined undefined
: undefined : undefined
} }
suggested={content.suggested} suggested={content.suggested}

View file

@ -13,7 +13,7 @@ import {
makeMentionCustomProps, makeMentionCustomProps,
renderMatrixMention, renderMatrixMention,
} from '../../plugins/react-custom-html-parser'; } from '../../plugins/react-custom-html-parser';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer'; import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room'; import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import { import {
@ -38,6 +38,7 @@ import { SequenceCard } from '../../components/sequence-card';
import { UserAvatar } from '../../components/user-avatar'; import { UserAvatar } from '../../components/user-avatar';
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler'; import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler'; import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type SearchResultGroupProps = { type SearchResultGroupProps = {
room: Room; room: Room;
@ -56,6 +57,7 @@ export function SearchResultGroup({
onOpen, onOpen,
}: SearchResultGroupProps) { }: SearchResultGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]); const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
const mentionClickHandler = useMentionClickHandler(room.roomId); const mentionClickHandler = useMentionClickHandler(room.roomId);
@ -75,10 +77,11 @@ export function SearchResultGroup({
getReactCustomHtmlParser(mx, room.roomId, { getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts, linkifyOpts,
highlightRegex, highlightRegex,
useAuthentication,
handleSpoilerClick: spoilerClickHandler, handleSpoilerClick: spoilerClickHandler,
handleMentionClick: mentionClickHandler, handleMentionClick: mentionClickHandler,
}), }),
[mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler] [mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler, useAuthentication]
); );
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>( const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
@ -161,7 +164,7 @@ export function SearchResultGroup({
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={getRoomAvatarUrl(mx, room, 96)} src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled /> <RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
@ -209,7 +212,7 @@ export function SearchResultGroup({
userId={event.sender} userId={event.sender}
src={ src={
senderAvatarMxc senderAvatarMxc
? mx.mxcUrlToHttp(senderAvatarMxc, 48, 48, 'crop') ?? undefined ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined : undefined
} }
alt={displayName} alt={displayName}

View file

@ -38,6 +38,7 @@ import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { getViaServers } from '../../plugins/via-servers'; import { getViaServers } from '../../plugins/via-servers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type RoomNavItemMenuProps = { type RoomNavItemMenuProps = {
room: Room; room: Room;
@ -175,6 +176,7 @@ export function RoomNavItem({
linkPath, linkPath,
}: RoomNavItemProps) { }: RoomNavItemProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
@ -217,7 +219,7 @@ export function RoomNavItem({
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={ src={
direct ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96) direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
} }
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (

View file

@ -55,6 +55,7 @@ import { ScrollTopContainer } from '../../components/scroll-top-container';
import { UserAvatar } from '../../components/user-avatar'; import { UserAvatar } from '../../components/user-avatar';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
export const MembershipFilters = { export const MembershipFilters = {
filterJoined: (m: RoomMember) => m.membership === Membership.Join, filterJoined: (m: RoomMember) => m.membership === Membership.Join,
@ -171,6 +172,7 @@ type MembersDrawerProps = {
}; };
export function MembersDrawer({ room, members }: MembersDrawerProps) { export function MembersDrawer({ room, members }: MembersDrawerProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null); const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
@ -426,9 +428,8 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
}} }}
after={<Icon size="50" src={Icons.Cross} />} after={<Icon size="50" src={Icons.Cross} />}
> >
<Text size="B300">{`${result.items.length || 'No'} ${ <Text size="B300">{`${result.items.length || 'No'} ${result.items.length === 1 ? 'Result' : 'Results'
result.items.length === 1 ? 'Result' : 'Results' }`}</Text>
}`}</Text>
</Chip> </Chip>
) )
} }
@ -483,14 +484,16 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const member = tagOrMember; const member = tagOrMember;
const name = getName(member); const name = getName(member);
const avatarUrl = member.getAvatarUrl( const avatarMxcUrl = member.getMxcAvatarUrl();
mx.baseUrl, const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(
avatarMxcUrl,
100, 100,
100, 100,
'crop', 'crop',
undefined, undefined,
false false,
); useAuthentication
) : undefined;
return ( return (
<MenuItem <MenuItem

View file

@ -13,7 +13,6 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../../client/action/notifications'; import { markAsRead } from '../../../client/action/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers'; import { useRoomMembers } from '../../hooks/useRoomMembers';
import { editableActiveElement } from '../../utils/dom';
export function Room() { export function Room() {
const { eventId } = useParams(); const { eventId } = useParams();
@ -29,7 +28,7 @@ export function Room() {
window, window,
useCallback( useCallback(
(evt) => { (evt) => {
if (isKeyHotkey('escape', evt) && !editableActiveElement()) { if (isKeyHotkey('escape', evt)) {
markAsRead(mx, room.roomId); markAsRead(mx, room.roomId);
} }
}, },

View file

@ -56,7 +56,7 @@ import {
} from '../../components/editor'; } from '../../components/editor';
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board'; import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix'; import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater'; import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
import { useFilePicker } from '../../hooks/useFilePicker'; import { useFilePicker } from '../../hooks/useFilePicker';
import { useFilePasteHandler } from '../../hooks/useFilePasteHandler'; import { useFilePasteHandler } from '../../hooks/useFilePasteHandler';
@ -108,6 +108,7 @@ import { mobileOrTablet } from '../../utils/user-agent';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { ReplyLayout, ThreadIndicator } from '../../components/message'; import { ReplyLayout, ThreadIndicator } from '../../components/message';
import { roomToParentsAtom } from '../../state/room/roomToParents'; import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
interface RoomInputProps { interface RoomInputProps {
editor: Editor; editor: Editor;
@ -118,6 +119,7 @@ interface RoomInputProps {
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>( export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
({ editor, fileDropContainerRef, roomId, room }, ref) => { ({ editor, fileDropContainerRef, roomId, room }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const commands = useCommands(mx, room); const commands = useCommands(mx, room);
@ -368,7 +370,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}; };
const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => { const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => {
const stickerUrl = mx.mxcUrlToHttp(mxc); const stickerUrl = mxcUrlToHttp(mx, mxc, useAuthentication);
if (!stickerUrl) return; if (!stickerUrl) return;
const info = await getImageInfo( const info = await getImageInfo(

View file

@ -17,7 +17,6 @@ import {
EventTimelineSet, EventTimelineSet,
EventTimelineSetHandlerMap, EventTimelineSetHandlerMap,
IContent, IContent,
IEncryptedFile,
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
Room, Room,
@ -48,12 +47,7 @@ import {
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { Opts as LinkifyOpts } from 'linkifyjs'; import { Opts as LinkifyOpts } from 'linkifyjs';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../utils/matrix';
decryptFile,
eventWithShortcode,
factoryEventSentBy,
getMxIdLocalPart,
} from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator'; import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
import { useAlive } from '../../hooks/useAlive'; import { useAlive } from '../../hooks/useAlive';
@ -122,6 +116,7 @@ import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler'; import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler'; import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const TimelineFloat = as<'div', css.TimelineFloatVariants>( const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => ( ({ position, className, ...props }, ref) => (
@ -219,18 +214,6 @@ export const getEventIdAbsoluteIndex = (
return baseIndex + eventIndex; return baseIndex + eventIndex;
}; };
export const factoryGetFileSrcUrl =
(httpUrl: string, mimeType: string, encFile?: IEncryptedFile) => async (): Promise<string> => {
if (encFile) {
if (typeof httpUrl !== 'string') throw new Error('Malformed event');
const encRes = await fetch(httpUrl, { method: 'GET' });
const encData = await encRes.arrayBuffer();
const decryptedBlob = await decryptFile(encData, mimeType, encFile);
return URL.createObjectURL(decryptedBlob);
}
return httpUrl;
};
type RoomTimelineProps = { type RoomTimelineProps = {
room: Room; room: Room;
eventId?: string; eventId?: string;
@ -437,6 +420,7 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) { export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const encryptedRoom = mx.isRoomEncrypted(room.roomId); const encryptedRoom = mx.isRoomEncrypted(room.roomId);
const [messageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing'); const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
@ -511,10 +495,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
() => () =>
getReactCustomHtmlParser(mx, room.roomId, { getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts, linkifyOpts,
useAuthentication,
handleSpoilerClick: spoilerClickHandler, handleSpoilerClick: spoilerClickHandler,
handleMentionClick: mentionClickHandler, handleMentionClick: mentionClickHandler,
}), }),
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler] [mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler, useAuthentication]
); );
const parseMemberEvent = useMemberEventParser(); const parseMemberEvent = useMemberEventParser();
@ -726,6 +711,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const editableEvtId = editableEvt?.getId(); const editableEvtId = editableEvt?.getId();
if (!editableEvtId) return; if (!editableEvtId) return;
setEditId(editableEvtId); setEditId(editableEvtId);
evt.preventDefault();
} }
}, },
[mx, room, editor] [mx, room, editor]

View file

@ -36,7 +36,7 @@ import { useSetSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils'; import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix'; import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
import { _SearchPathSearchParams } from '../../pages/paths'; import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css'; import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
@ -53,6 +53,7 @@ import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getViaServers } from '../../plugins/via-servers'; import { getViaServers } from '../../plugins/via-servers';
import { BackRouteHandler } from '../../components/BackRouteHandler'; import { BackRouteHandler } from '../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
@ -174,6 +175,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
export function RoomViewHeader() { export function RoomViewHeader() {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const room = useRoom(); const room = useRoom();
const space = useSpaceOptionally(); const space = useSpaceOptionally();
@ -185,7 +187,7 @@ export function RoomViewHeader() {
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const name = useRoomName(room); const name = useRoomName(room);
const topic = useRoomTopic(room); const topic = useRoomTopic(room);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');

View file

@ -51,7 +51,7 @@ import {
getMemberAvatarMxc, getMemberAvatarMxc,
getMemberDisplayName, getMemberDisplayName,
} from '../../../utils/room'; } from '../../../utils/room';
import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias } from '../../../utils/matrix'; import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias, mxcUrlToHttp } from '../../../utils/matrix';
import { MessageLayout, MessageSpacing } from '../../../state/settings'; import { MessageLayout, MessageSpacing } from '../../../state/settings';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
@ -67,6 +67,7 @@ import { copyToClipboard } from '../../../utils/dom';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoomEvent } from '../../../plugins/matrix-to'; import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers'; import { getViaServers } from '../../../plugins/via-servers';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@ -234,9 +235,9 @@ export const MessageSourceCodeItem = as<
const getContent = (evt: MatrixEvent) => const getContent = (evt: MatrixEvent) =>
evt.isEncrypted() evt.isEncrypted()
? { ? {
[`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(), [`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(),
[`<== ORIGINAL_EVENT ==>`]: evt.event, [`<== ORIGINAL_EVENT ==>`]: evt.event,
} }
: evt.event; : evt.event;
const getText = (): string => { const getText = (): string => {
@ -650,6 +651,7 @@ export const Message = as<'div', MessageProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const senderId = mEvent.getSender() ?? ''; const senderId = mEvent.getSender() ?? '';
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
@ -709,7 +711,7 @@ export const Message = as<'div', MessageProps>(
userId={senderId} userId={senderId}
src={ src={
senderAvatarMxc senderAvatarMxc
? mx.mxcUrlToHttp(senderAvatarMxc, 48, 48, 'crop') ?? undefined ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined : undefined
} }
alt={senderDisplayName} alt={senderDisplayName}
@ -950,26 +952,26 @@ export const Message = as<'div', MessageProps>(
</Box> </Box>
{((!mEvent.isRedacted() && canDelete) || {((!mEvent.isRedacted() && canDelete) ||
mEvent.getSender() !== mx.getUserId()) && ( mEvent.getSender() !== mx.getUserId()) && (
<> <>
<Line size="300" /> <Line size="300" />
<Box direction="Column" gap="100" className={css.MessageMenuGroup}> <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
{!mEvent.isRedacted() && canDelete && ( {!mEvent.isRedacted() && canDelete && (
<MessageDeleteItem <MessageDeleteItem
room={room} room={room}
mEvent={mEvent} mEvent={mEvent}
onClose={closeMenu} onClose={closeMenu}
/> />
)} )}
{mEvent.getSender() !== mx.getUserId() && ( {mEvent.getSender() !== mx.getUserId() && (
<MessageReportItem <MessageReportItem
room={room} room={room}
mEvent={mEvent} mEvent={mEvent}
onClose={closeMenu} onClose={closeMenu}
/> />
)} )}
</Box> </Box>
</> </>
)} )}
</Menu> </Menu>
</FocusTrap> </FocusTrap>
} }
@ -1093,26 +1095,26 @@ export const Event = as<'div', EventProps>(
</Box> </Box>
{((!mEvent.isRedacted() && canDelete && !stateEvent) || {((!mEvent.isRedacted() && canDelete && !stateEvent) ||
(mEvent.getSender() !== mx.getUserId() && !stateEvent)) && ( (mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (
<> <>
<Line size="300" /> <Line size="300" />
<Box direction="Column" gap="100" className={css.MessageMenuGroup}> <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
{!mEvent.isRedacted() && canDelete && ( {!mEvent.isRedacted() && canDelete && (
<MessageDeleteItem <MessageDeleteItem
room={room} room={room}
mEvent={mEvent} mEvent={mEvent}
onClose={closeMenu} onClose={closeMenu}
/> />
)} )}
{mEvent.getSender() !== mx.getUserId() && ( {mEvent.getSender() !== mx.getUserId() && (
<MessageReportItem <MessageReportItem
room={room} room={room}
mEvent={mEvent} mEvent={mEvent}
onClose={closeMenu} onClose={closeMenu}
/> />
)} )}
</Box> </Box>
</> </>
)} )}
</Menu> </Menu>
</FocusTrap> </FocusTrap>
} }

View file

@ -22,6 +22,7 @@ import { useRelations } from '../../../hooks/useRelations';
import * as css from './styles.css'; import * as css from './styles.css';
import { ReactionViewer } from '../reaction-viewer'; import { ReactionViewer } from '../reaction-viewer';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
export type ReactionsProps = { export type ReactionsProps = {
room: Room; room: Room;
@ -33,6 +34,7 @@ export type ReactionsProps = {
export const Reactions = as<'div', ReactionsProps>( export const Reactions = as<'div', ReactionsProps>(
({ className, room, relations, mEventId, canSendReaction, onReactionToggle, ...props }, ref) => { ({ className, room, relations, mEventId, canSendReaction, onReactionToggle, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState<boolean | string>(false); const [viewer, setViewer] = useState<boolean | string>(false);
const myUserId = mx.getUserId(); const myUserId = mx.getUserId();
const reactions = useRelations( const reactions = useRelations(
@ -86,6 +88,7 @@ export const Reactions = as<'div', ReactionsProps>(
onClick={canSendReaction ? () => onReactionToggle(mEventId, key) : undefined} onClick={canSendReaction ? () => onReactionToggle(mEventId, key) : undefined}
onContextMenu={handleViewReaction} onContextMenu={handleViewReaction}
aria-disabled={!canSendReaction} aria-disabled={!canSendReaction}
useAuthentication={useAuthentication}
/> />
)} )}
</TooltipProvider> </TooltipProvider>

View file

@ -25,6 +25,7 @@ import { useRelations } from '../../../hooks/useRelations';
import { Reaction } from '../../../components/message'; import { Reaction } from '../../../components/message';
import { getHexcodeForEmoji, getShortcodeFor } from '../../../plugins/emoji'; import { getHexcodeForEmoji, getShortcodeFor } from '../../../plugins/emoji';
import { UserAvatar } from '../../../components/user-avatar'; import { UserAvatar } from '../../../components/user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
export type ReactionViewerProps = { export type ReactionViewerProps = {
room: Room; room: Room;
@ -35,6 +36,7 @@ export type ReactionViewerProps = {
export const ReactionViewer = as<'div', ReactionViewerProps>( export const ReactionViewer = as<'div', ReactionViewerProps>(
({ className, room, initialKey, relations, requestClose, ...props }, ref) => { ({ className, room, initialKey, relations, requestClose, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const reactions = useRelations( const reactions = useRelations(
relations, relations,
useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], []) useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
@ -81,6 +83,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
count={evts.size} count={evts.size}
aria-selected={key === selectedKey} aria-selected={key === selectedKey}
onClick={() => setSelectedKey(key)} onClick={() => setSelectedKey(key)}
useAuthentication={useAuthentication}
/> />
); );
})} })}
@ -107,14 +110,16 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
const member = room.getMember(senderId); const member = room.getMember(senderId);
const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId; const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId;
const avatarUrl = member?.getAvatarUrl( const avatarMxcUrl = member?.getMxcAvatarUrl();
mx.baseUrl, const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(
avatarMxcUrl,
100, 100,
100, 100,
'crop', 'crop',
undefined, undefined,
false false,
); useAuthentication
) : undefined;
return ( return (
<MenuItem <MenuItem

View file

@ -0,0 +1,11 @@
import { useSpecVersions } from './useSpecVersions';
export const useMediaAuthentication = (): boolean => {
const { versions, unstable_features: unstableFeatures } = useSpecVersions();
// Media authentication is introduced in spec version 1.11
const authenticatedMedia =
unstableFeatures?.['org.matrix.msc3916.stable'] || versions.includes('v1.11');
return authenticatedMedia;
};

View file

@ -14,6 +14,7 @@ export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLEl
const handleClick: ReactEventHandler<HTMLElement> = useCallback( const handleClick: ReactEventHandler<HTMLElement> = useCallback(
(evt) => { (evt) => {
evt.stopPropagation();
evt.preventDefault(); evt.preventDefault();
const target = evt.currentTarget; const target = evt.currentTarget;
const mentionId = target.getAttribute('data-mention-id'); const mentionId = target.getAttribute('data-mention-id');

View file

@ -1,16 +1,10 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth'; import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth';
import { WithRequiredProp } from '../../types/utils';
export type Required_SSOFlow = WithRequiredProp<ISSOFlow, 'identity_providers'>; export const getSSOFlow = (loginFlows: LoginFlow[]): ISSOFlow | undefined =>
export const getSSOFlow = (loginFlows: LoginFlow[]): Required_SSOFlow | undefined => loginFlows.find((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas') as
loginFlows.find( | ISSOFlow
(flow) => | undefined;
(flow.type === 'm.login.sso' || flow.type === 'm.login.cas') &&
'identity_providers' in flow &&
Array.isArray(flow.identity_providers) &&
flow.identity_providers.length > 0
) as Required_SSOFlow | undefined;
export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined => export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined =>
loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow; loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow;
@ -22,7 +16,7 @@ export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
export type ParsedLoginFlows = { export type ParsedLoginFlows = {
password?: LoginFlow; password?: LoginFlow;
token?: LoginFlow; token?: LoginFlow;
sso?: Required_SSOFlow; sso?: ISSOFlow;
}; };
export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => { export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => {
const parsedFlow: ParsedLoginFlows = useMemo<ParsedLoginFlows>( const parsedFlow: ParsedLoginFlows = useMemo<ParsedLoginFlows>(

View file

@ -1,6 +1,4 @@
import React, { import React, { useState, useMemo, useReducer, useEffect } from 'react';
useState, useMemo, useReducer, useEffect,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './ImagePack.scss'; import './ImagePack.scss';
@ -19,41 +17,41 @@ import ImagePackProfile from './ImagePackProfile';
import ImagePackItem from './ImagePackItem'; import ImagePackItem from './ImagePackItem';
import ImagePackUpload from './ImagePackUpload'; import ImagePackUpload from './ImagePackUpload';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const renameImagePackItem = (shortcode) => new Promise((resolve) => { const renameImagePackItem = (shortcode) =>
let isCompleted = false; new Promise((resolve) => {
let isCompleted = false;
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">Rename</Text>, <Text variant="s1" weight="medium">
(requestClose) => ( Rename
<div style={{ padding: 'var(--sp-normal)' }}> </Text>,
<form (requestClose) => (
onSubmit={(e) => { <div style={{ padding: 'var(--sp-normal)' }}>
e.preventDefault(); <form
const sc = e.target.shortcode.value; onSubmit={(e) => {
if (sc.trim() === '') return; e.preventDefault();
isCompleted = true; const sc = e.target.shortcode.value;
resolve(sc.trim()); if (sc.trim() === '') return;
requestClose(); isCompleted = true;
}} resolve(sc.trim());
> requestClose();
<Input }}
value={shortcode} >
name="shortcode" <Input value={shortcode} name="shortcode" label="Shortcode" autoFocus required />
label="Shortcode" <div style={{ height: 'var(--sp-normal)' }} />
autoFocus <Button variant="primary" type="submit">
required Rename
/> </Button>
<div style={{ height: 'var(--sp-normal)' }} /> </form>
<Button variant="primary" type="submit">Rename</Button> </div>
</form> ),
</div> () => {
), if (!isCompleted) resolve(null);
() => { }
if (!isCompleted) resolve(null); );
}, });
);
});
function getUsage(usage) { function getUsage(usage) {
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both'; if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
@ -79,7 +77,7 @@ function useRoomImagePack(roomId, stateKey) {
const pack = useMemo(() => { const pack = useMemo(() => {
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent()) return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent());
}, [room, stateKey]); }, [room, stateKey]);
const sendPackContent = (content) => { const sendPackContent = (content) => {
@ -96,10 +94,13 @@ function useUserImagePack() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const pack = useMemo(() => { const pack = useMemo(() => {
const packEvent = mx.getAccountData('im.ponies.user_emotes'); const packEvent = mx.getAccountData('im.ponies.user_emotes');
return ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? { return ImagePackBuilder.parsePack(
pack: { display_name: 'Personal' }, mx.getUserId(),
images: {}, packEvent?.getContent() ?? {
}) pack: { display_name: 'Personal' },
images: {},
}
);
}, [mx]); }, [mx]);
const sendPackContent = (content) => { const sendPackContent = (content) => {
@ -119,10 +120,7 @@ function useImagePackHandles(pack, sendPackContent) {
if (typeof key !== 'string') return undefined; if (typeof key !== 'string') return undefined;
let newKey = key?.replace(/\s/g, '_'); let newKey = key?.replace(/\s/g, '_');
if (pack.getImages().get(newKey)) { if (pack.getImages().get(newKey)) {
newKey = suffixRename( newKey = suffixRename(newKey, (suffixedKey) => pack.getImages().get(suffixedKey));
newKey,
(suffixedKey) => pack.getImages().get(suffixedKey),
);
} }
return newKey; return newKey;
}; };
@ -163,7 +161,7 @@ function useImagePackHandles(pack, sendPackContent) {
'Delete', 'Delete',
`Are you sure that you want to delete "${key}"?`, `Are you sure that you want to delete "${key}"?`,
'Delete', 'Delete',
'danger', 'danger'
); );
if (!isConfirmed) return; if (!isConfirmed) return;
pack.removeImage(key); pack.removeImage(key);
@ -226,6 +224,7 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
const [viewMore, setViewMore] = useState(false); const [viewMore, setViewMore] = useState(false);
const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey)); const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey));
const useAuthentication = useMediaAuthentication();
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey); const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
@ -253,7 +252,7 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
'Delete Pack', 'Delete Pack',
`Are you sure that you want to delete "${pack.displayName}"?`, `Are you sure that you want to delete "${pack.displayName}"?`,
'Delete', 'Delete',
'danger', 'danger'
); );
if (!isConfirmed) return; if (!isConfirmed) return;
handlePackDelete(stateKey); handlePackDelete(stateKey);
@ -264,7 +263,19 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
return ( return (
<div className="image-pack"> <div className="image-pack">
<ImagePackProfile <ImagePackProfile
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null} avatarUrl={
pack.avatarUrl
? mx.mxcUrlToHttp(
pack.avatarUrl,
42,
42,
'crop',
undefined,
undefined,
useAuthentication
)
: null
}
displayName={pack.displayName ?? 'Unknown'} displayName={pack.displayName ?? 'Unknown'}
attribution={pack.attribution} attribution={pack.attribution}
usage={getUsage(pack.usage)} usage={getUsage(pack.usage)}
@ -272,10 +283,8 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
onAvatarChange={canChange ? handleAvatarChange : null} onAvatarChange={canChange ? handleAvatarChange : null}
onEditProfile={canChange ? handleEditProfile : null} onEditProfile={canChange ? handleEditProfile : null}
/> />
{ canChange && ( {canChange && <ImagePackUpload onUpload={handleAddItem} />}
<ImagePackUpload onUpload={handleAddItem} /> {images.length === 0 ? null : (
)}
{ images.length === 0 ? null : (
<div> <div>
<div className="image-pack__header"> <div className="image-pack__header">
<Text variant="b3">Image</Text> <Text variant="b3">Image</Text>
@ -285,7 +294,15 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
{images.map(([shortcode, image]) => ( {images.map(([shortcode, image]) => (
<ImagePackItem <ImagePackItem
key={shortcode} key={shortcode}
url={mx.mxcUrlToHttp(image.mxc)} url={mx.mxcUrlToHttp(
image.mxc,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthentication
)}
shortcode={shortcode} shortcode={shortcode}
usage={getUsage(image.usage)} usage={getUsage(image.usage)}
onUsageChange={canChange ? handleUsageItem : undefined} onUsageChange={canChange ? handleUsageItem : undefined}
@ -299,14 +316,14 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
<div className="image-pack__footer"> <div className="image-pack__footer">
{pack.images.size > 2 && ( {pack.images.size > 2 && (
<Button onClick={() => setViewMore(!viewMore)}> <Button onClick={() => setViewMore(!viewMore)}>
{ {viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
viewMore </Button>
? 'View less' )}
: `View ${pack.images.size - 2} more` {handlePackDelete && (
} <Button variant="danger" onClick={handleDeletePack}>
Delete Pack
</Button> </Button>
)} )}
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
</div> </div>
)} )}
<div className="image-pack__global"> <div className="image-pack__global">
@ -332,6 +349,7 @@ ImagePack.propTypes = {
function ImagePackUser() { function ImagePackUser() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [viewMore, setViewMore] = useState(false); const [viewMore, setViewMore] = useState(false);
const useAuthentication = useMediaAuthentication();
const { pack, sendPackContent } = useUserImagePack(); const { pack, sendPackContent } = useUserImagePack();
@ -350,7 +368,19 @@ function ImagePackUser() {
return ( return (
<div className="image-pack"> <div className="image-pack">
<ImagePackProfile <ImagePackProfile
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null} avatarUrl={
pack.avatarUrl
? mx.mxcUrlToHttp(
pack.avatarUrl,
42,
42,
'crop',
undefined,
undefined,
useAuthentication
)
: null
}
displayName={pack.displayName ?? 'Personal'} displayName={pack.displayName ?? 'Personal'}
attribution={pack.attribution} attribution={pack.attribution}
usage={getUsage(pack.usage)} usage={getUsage(pack.usage)}
@ -359,7 +389,7 @@ function ImagePackUser() {
onEditProfile={handleEditProfile} onEditProfile={handleEditProfile}
/> />
<ImagePackUpload onUpload={handleAddItem} /> <ImagePackUpload onUpload={handleAddItem} />
{ images.length === 0 ? null : ( {images.length === 0 ? null : (
<div> <div>
<div className="image-pack__header"> <div className="image-pack__header">
<Text variant="b3">Image</Text> <Text variant="b3">Image</Text>
@ -369,7 +399,15 @@ function ImagePackUser() {
{images.map(([shortcode, image]) => ( {images.map(([shortcode, image]) => (
<ImagePackItem <ImagePackItem
key={shortcode} key={shortcode}
url={mx.mxcUrlToHttp(image.mxc)} url={mx.mxcUrlToHttp(
image.mxc,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthentication
)}
shortcode={shortcode} shortcode={shortcode}
usage={getUsage(image.usage)} usage={getUsage(image.usage)}
onUsageChange={handleUsageItem} onUsageChange={handleUsageItem}
@ -379,14 +417,10 @@ function ImagePackUser() {
))} ))}
</div> </div>
)} )}
{(pack.images.size > 2) && ( {pack.images.size > 2 && (
<div className="image-pack__footer"> <div className="image-pack__footer">
<Button onClick={() => setViewMore(!viewMore)}> <Button onClick={() => setViewMore(!viewMore)}>
{ {viewMore ? 'View less' : `View ${pack.images.size - 2} more`}
viewMore
? 'View less'
: `View ${pack.images.size - 2} more`
}
</Button> </Button>
</div> </div>
)} )}
@ -435,29 +469,33 @@ function ImagePackGlobal() {
<div className="image-pack-global"> <div className="image-pack-global">
<MenuHeader>Global packs</MenuHeader> <MenuHeader>Global packs</MenuHeader>
<div> <div>
{ {roomIdToStateKeys.size > 0 ? (
roomIdToStateKeys.size > 0 [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => { const room = mx.getRoom(roomId);
const room = mx.getRoom(roomId); return stateKeys.map((stateKey) => {
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
if (!pack) return null;
return ( return (
stateKeys.map((stateKey) => { <div className="image-pack__global" key={pack.id}>
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey); <Checkbox
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent()); variant="positive"
if (!pack) return null; onToggle={() => handleChange(roomId, stateKey)}
return ( isActive
<div className="image-pack__global" key={pack.id}> />
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive /> <div>
<div> <Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text> <Text variant="b3">{room.name}</Text>
<Text variant="b3">{room.name}</Text> </div>
</div> </div>
</div>
);
})
); );
}) });
: <div className="image-pack-global__empty"><Text>No global packs</Text></div> })
} ) : (
<div className="image-pack-global__empty">
<Text>No global packs</Text>
</div>
)}
</div> </div>
</div> </div>
); );

View file

@ -18,11 +18,13 @@ import UserIC from '../../../../public/res/ic/outlined/user.svg';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getDMRoomFor } from '../../utils/matrix'; import { getDMRoomFor } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) { function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
const [isSearching, updateIsSearching] = useState(false); const [isSearching, updateIsSearching] = useState(false);
const [searchQuery, updateSearchQuery] = useState({}); const [searchQuery, updateSearchQuery] = useState({});
const [users, updateUsers] = useState([]); const [users, updateUsers] = useState([]);
const useAuthentication = useMediaAuthentication();
const [procUsers, updateProcUsers] = useState(new Set()); // proc stands for processing. const [procUsers, updateProcUsers] = useState(new Set()); // proc stands for processing.
const [procUserError, updateUserProcError] = useState(new Map()); const [procUserError, updateUserProcError] = useState(new Map());
@ -222,7 +224,15 @@ function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
key={userId} key={userId}
avatarSrc={ avatarSrc={
typeof user.avatar_url === 'string' typeof user.avatar_url === 'string'
? mx.mxcUrlToHttp(user.avatar_url, 42, 42, 'crop') ? mx.mxcUrlToHttp(
user.avatar_url,
42,
42,
'crop',
undefined,
undefined,
useAuthentication
)
: null : null
} }
name={name} name={name}

View file

@ -14,15 +14,19 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import './ProfileEditor.scss'; import './ProfileEditor.scss';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function ProfileEditor({ userId }) { function ProfileEditor({ userId }) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const mx = useMatrixClient(); const mx = useMatrixClient();
const user = mx.getUser(mx.getUserId()); const user = mx.getUser(mx.getUserId());
const useAuthentication = useMediaAuthentication();
const displayNameRef = useRef(null); const displayNameRef = useRef(null);
const [avatarSrc, setAvatarSrc] = useState( const [avatarSrc, setAvatarSrc] = useState(
user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null user.avatarUrl
? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop', undefined, undefined, useAuthentication)
: null
); );
const [username, setUsername] = useState(user.displayName); const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true); const [disabled, setDisabled] = useState(true);
@ -31,13 +35,25 @@ function ProfileEditor({ userId }) {
let isMounted = true; let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => { mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return; if (!isMounted) return;
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null); setAvatarSrc(
info.avatar_url
? mx.mxcUrlToHttp(
info.avatar_url,
80,
80,
'crop',
undefined,
undefined,
useAuthentication
)
: null
);
setUsername(info.displayname); setUsername(info.displayname);
}); });
return () => { return () => {
isMounted = false; isMounted = false;
}; };
}, [mx, userId]); }, [mx, userId, useAuthentication]);
const handleAvatarUpload = async (url) => { const handleAvatarUpload = async (url) => {
if (url === null) { if (url === null) {
@ -54,7 +70,7 @@ function ProfileEditor({ userId }) {
return; return;
} }
mx.setAvatarUrl(url); mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop')); setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop', undefined, undefined, useAuthentication));
}; };
const saveDisplayName = () => { const saveDisplayName = () => {

View file

@ -36,6 +36,7 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getDMRoomFor } from '../../utils/matrix'; import { getDMRoomFor } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function ModerationTools({ roomId, userId }) { function ModerationTools({ roomId, userId }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
@ -329,6 +330,7 @@ function useRerenderOnProfileChange(roomId, userId) {
function ProfileViewer() { function ProfileViewer() {
const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog(); const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog();
useRerenderOnProfileChange(roomId, userId); useRerenderOnProfileChange(roomId, userId);
const useAuthentication = useMediaAuthentication();
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@ -338,7 +340,9 @@ function ProfileViewer() {
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId); const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId);
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl; const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
const avatarUrl = const avatarUrl =
avatarMxc && avatarMxc !== 'null' ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null; avatarMxc && avatarMxc !== 'null'
? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop', undefined, undefined, useAuthentication)
: null;
const powerLevel = roomMember?.powerLevel || 0; const powerLevel = roomMember?.powerLevel || 0;
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0; const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;

View file

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

View file

@ -4,69 +4,89 @@ import React, { useMemo } from 'react';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
type SSOLoginProps = { type SSOLoginProps = {
providers: IIdentityProvider[]; providers?: IIdentityProvider[];
asIcons?: boolean;
redirectUrl: string; redirectUrl: string;
saveScreenSpace?: boolean;
}; };
export function SSOLogin({ providers, redirectUrl, asIcons }: SSOLoginProps) { export function SSOLogin({ providers, redirectUrl, saveScreenSpace }: SSOLoginProps) {
const discovery = useAutoDiscoveryInfo(); const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url; const baseUrl = discovery['m.homeserver'].base_url;
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
const getSSOIdUrl = (ssoId: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); const getSSOIdUrl = (ssoId?: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId);
const anyAsBtn = providers.find( const withoutIcon = providers
(provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false) ? providers.find(
); (provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false)
)
: true;
const renderAsIcons = withoutIcon ? false : saveScreenSpace && providers && providers.length > 2;
return ( return (
<Box justifyContent="Center" gap="600" wrap="Wrap"> <Box justifyContent="Center" gap="600" wrap="Wrap">
{providers.map((provider) => { {providers ? (
const { id, name, icon } = provider; providers.map((provider) => {
const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false); const { id, name, icon } = provider;
const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false);
const buttonTitle = `Continue with ${name}`; const buttonTitle = `Continue with ${name}`;
if (renderAsIcons) {
return (
<Avatar
style={{ cursor: 'pointer' }}
key={id}
as="a"
href={getSSOIdUrl(id)}
aria-label={buttonTitle}
size="300"
radii="300"
>
<AvatarImage src={iconUrl!} alt={name} title={buttonTitle} />
</Avatar>
);
}
if (!anyAsBtn && iconUrl && asIcons) {
return ( return (
<Avatar <Button
style={{ cursor: 'pointer' }} style={{ width: '100%' }}
key={id} key={id}
as="a" as="a"
href={getSSOIdUrl(id)} href={getSSOIdUrl(id)}
aria-label={buttonTitle} size="500"
size="300" variant="Secondary"
radii="300" fill="Soft"
outlined
before={
iconUrl && (
<Avatar size="200" radii="300">
<AvatarImage src={iconUrl} alt={name} />
</Avatar>
)
}
> >
<AvatarImage src={iconUrl} alt={name} title={buttonTitle} /> <Text align="Center" size="B500" truncate>
</Avatar> {buttonTitle}
</Text>
</Button>
); );
} })
) : (
return ( <Button
<Button style={{ width: '100%' }}
style={{ width: '100%' }} as="a"
key={id} href={getSSOIdUrl()}
as="a" size="500"
href={getSSOIdUrl(id)} variant="Secondary"
size="500" fill="Soft"
variant="Secondary" outlined
fill="Soft" >
outlined <Text align="Center" size="B500" truncate>
before={ Continue with SSO
iconUrl && ( </Text>
<Avatar size="200" radii="300"> </Button>
<AvatarImage src={iconUrl} alt={name} /> )}
</Avatar>
)
}
>
<Text align="Center" size="B500" truncate>
{buttonTitle}
</Text>
</Button>
);
})}
</Box> </Box>
); );
} }

View file

@ -76,9 +76,7 @@ export function Login() {
<SSOLogin <SSOLogin
providers={parsedFlows.sso.identity_providers} providers={parsedFlows.sso.identity_providers}
redirectUrl={ssoRedirectUrl} redirectUrl={ssoRedirectUrl}
asIcons={ saveScreenSpace={parsedFlows.password !== undefined}
parsedFlows.password !== undefined && parsedFlows.sso.identity_providers.length > 2
}
/> />
<span data-spacing-node /> <span data-spacing-node />
</> </>

View file

@ -83,10 +83,7 @@ export function Register() {
<SSOLogin <SSOLogin
providers={sso.identity_providers} providers={sso.identity_providers}
redirectUrl={ssoRedirectUrl} redirectUrl={ssoRedirectUrl}
asIcons={ saveScreenSpace={registerFlows.status === RegisterFlowStatus.FlowRequired}
registerFlows.status === RegisterFlowStatus.FlowRequired &&
sso.identity_providers.length > 2
}
/> />
<span data-spacing-node /> <span data-spacing-node />
</> </>

View file

@ -22,9 +22,10 @@ import {
isNotificationEvent, isNotificationEvent,
} from '../../utils/room'; } from '../../utils/room';
import { NotificationType, UnreadInfo } from '../../../types/matrix/room'; import { NotificationType, UnreadInfo } from '../../../types/matrix/room';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
import { useInboxNotificationsSelected } from '../../hooks/router/useInbox'; import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function SystemEmojiFeature() { function SystemEmojiFeature() {
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
@ -132,6 +133,7 @@ function MessageNotifications() {
const notifRef = useRef<Notification>(); const notifRef = useRef<Notification>();
const unreadCacheRef = useRef<Map<string, UnreadInfo>>(new Map()); const unreadCacheRef = useRef<Map<string, UnreadInfo>>(new Map());
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [showNotifications] = useSetting(settingsAtom, 'showNotifications'); const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
@ -216,7 +218,7 @@ function MessageNotifications() {
notify({ notify({
roomName: room.name ?? 'Unknown', roomName: room.name ?? 'Unknown',
roomAvatar: avatarMxc roomAvatar: avatarMxc
? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined, : undefined,
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender, username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
roomId: room.roomId, roomId: room.roomId,
@ -240,6 +242,7 @@ function MessageNotifications() {
playSound, playSound,
notify, notify,
selectedRoomId, selectedRoomId,
useAuthentication,
]); ]);
return ( return (

View file

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

View file

@ -42,6 +42,7 @@ import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useRoomTopic } from '../../../hooks/useRoomMeta'; import { useRoomTopic } from '../../../hooks/useRoomMeta';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
const COMPACT_CARD_WIDTH = 548; const COMPACT_CARD_WIDTH = 548;
@ -54,6 +55,7 @@ type InviteCardProps = {
}; };
function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardProps) { function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const roomName = room.name || room.getCanonicalAlias() || room.roomId; const roomName = room.name || room.getCanonicalAlias() || room.roomId;
const member = room.getMember(userId); const member = room.getMember(userId);
const memberEvent = member?.events.member; const memberEvent = member?.events.member;
@ -110,7 +112,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
<Avatar size="300"> <Avatar size="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={direct ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96)} src={direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={roomName} alt={roomName}
renderFallback={() => ( renderFallback={() => (
<Text as="span" size="H6"> <Text as="span" size="H6">

View file

@ -28,7 +28,7 @@ import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts as LinkifyOpts } from 'linkifyjs'; import { Opts as LinkifyOpts } from 'linkifyjs';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page'; import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart } from '../../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { InboxNotificationsPathSearchParams } from '../../paths'; import { InboxNotificationsPathSearchParams } from '../../paths';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
@ -81,6 +81,7 @@ import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler'; import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type RoomNotificationsGroup = { type RoomNotificationsGroup = {
roomId: string; roomId: string;
@ -191,6 +192,7 @@ function RoomNotificationsGroupComp({
onOpen, onOpen,
}: RoomNotificationsGroupProps) { }: RoomNotificationsGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const mentionClickHandler = useMentionClickHandler(room.roomId); const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler(); const spoilerClickHandler = useSpoilerClickHandler();
@ -208,10 +210,11 @@ function RoomNotificationsGroupComp({
() => () =>
getReactCustomHtmlParser(mx, room.roomId, { getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts, linkifyOpts,
useAuthentication,
handleSpoilerClick: spoilerClickHandler, handleSpoilerClick: spoilerClickHandler,
handleMentionClick: mentionClickHandler, handleMentionClick: mentionClickHandler,
}), }),
[mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler] [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
); );
const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>( const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>(
@ -369,7 +372,7 @@ function RoomNotificationsGroupComp({
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={getRoomAvatarUrl(mx, room, 96)} src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled /> <RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
@ -424,7 +427,7 @@ function RoomNotificationsGroupComp({
userId={event.sender} userId={event.sender}
src={ src={
senderAvatarMxc senderAvatarMxc
? mx.mxcUrlToHttp(senderAvatarMxc, 48, 48, 'crop') ?? undefined ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined : undefined
} }
alt={displayName} alt={displayName}

View file

@ -86,6 +86,8 @@ import { openInviteUser, openSpaceSettings } from '../../../../client/action/nav
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to'; import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers'; import { getViaServers } from '../../../plugins/via-servers';
import { getRoomAvatarUrl } from '../../../utils/room';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type SpaceMenuProps = { type SpaceMenuProps = {
room: Room; room: Room;
@ -225,18 +227,18 @@ const useDraggableItem = (
return !target return !target
? undefined ? undefined
: draggable({ : draggable({
element: target, element: target,
dragHandle, dragHandle,
getInitialData: () => ({ item }), getInitialData: () => ({ item }),
onDragStart: () => { onDragStart: () => {
setDragging(true); setDragging(true);
onDragging?.(item); onDragging?.(item);
}, },
onDrop: () => { onDrop: () => {
setDragging(false); setDragging(false);
onDragging?.(undefined); onDragging?.(undefined);
}, },
}); });
}, [targetRef, dragHandleRef, item, onDragging]); }, [targetRef, dragHandleRef, item, onDragging]);
return dragging; return dragging;
@ -379,15 +381,16 @@ function SpaceTab({
onUnpin, onUnpin,
}: SpaceTabProps) { }: SpaceTabProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
const spaceDraggable: SidebarDraggable = useMemo( const spaceDraggable: SidebarDraggable = useMemo(
() => () =>
folder folder
? { ? {
folder, folder,
spaceId: space.roomId, spaceId: space.roomId,
} }
: space.roomId, : space.roomId,
[folder, space] [folder, space]
); );
@ -431,7 +434,7 @@ function SpaceTab({
> >
<RoomAvatar <RoomAvatar
roomId={space.roomId} roomId={space.roomId}
src={space.getAvatarUrl(mx.baseUrl, 96, 96, 'crop') ?? undefined} src={getRoomAvatarUrl(mx, space, 96, useAuthentication) ?? undefined}
alt={space.name} alt={space.name}
renderFallback={() => ( renderFallback={() => (
<Text size={folder ? 'H6' : 'H4'}>{nameInitials(space.name, 2)}</Text> <Text size={folder ? 'H6' : 'H4'}>{nameInitials(space.name, 2)}</Text>
@ -524,6 +527,7 @@ function ClosedSpaceFolder({
disabled, disabled,
}: ClosedSpaceFolderProps) { }: ClosedSpaceFolderProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const handlerRef = useRef<HTMLDivElement>(null); const handlerRef = useRef<HTMLDivElement>(null);
const spaceDraggable: FolderDraggable = useMemo(() => ({ folder }), [folder]); const spaceDraggable: FolderDraggable = useMemo(() => ({ folder }), [folder]);
@ -556,7 +560,7 @@ function ClosedSpaceFolder({
<SidebarAvatar key={sId} size="200" radii="300"> <SidebarAvatar key={sId} size="200" radii="300">
<RoomAvatar <RoomAvatar
roomId={space.roomId} roomId={space.roomId}
src={space.getAvatarUrl(mx.baseUrl, 96, 96, 'crop') ?? undefined} src={getRoomAvatarUrl(mx, space, 96, useAuthentication) ?? undefined}
alt={space.name} alt={space.name}
renderFallback={() => ( renderFallback={() => (
<Text size="Inherit"> <Text size="Inherit">

View file

@ -5,8 +5,9 @@ import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../compone
import { openSettings } from '../../../../client/action/navigation'; import { openSettings } from '../../../../client/action/navigation';
import { UserAvatar } from '../../../components/user-avatar'; import { UserAvatar } from '../../../components/user-avatar';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart } from '../../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { nameInitials } from '../../../utils/common'; import { nameInitials } from '../../../utils/common';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type UserProfile = { type UserProfile = {
avatar_url?: string; avatar_url?: string;
@ -14,12 +15,13 @@ type UserProfile = {
}; };
export function UserTab() { export function UserTab() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const userId = mx.getUserId()!; const userId = mx.getUserId()!;
const [profile, setProfile] = useState<UserProfile>({}); const [profile, setProfile] = useState<UserProfile>({});
const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId; const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatar_url const avatarUrl = profile.avatar_url
? mx.mxcUrlToHttp(profile.avatar_url, 96, 96, 'crop') ?? undefined ? mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined; : undefined;
useEffect(() => { useEffect(() => {

View file

@ -14,7 +14,12 @@ import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyj
import Linkify from 'linkify-react'; import Linkify from 'linkify-react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import * as css from '../styles/CustomHtml.css'; import * as css from '../styles/CustomHtml.css';
import { getMxIdLocalPart, getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix'; import {
getMxIdLocalPart,
getCanonicalAliasRoomId,
isRoomAlias,
mxcUrlToHttp,
} from '../utils/matrix';
import { getMemberDisplayName } from '../utils/room'; import { getMemberDisplayName } from '../utils/room';
import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex'; import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex';
import { getHexcodeForEmoji, getShortcodeFor, getEmojiUrl, isUsingTwemoji } from './emoji'; import { getHexcodeForEmoji, getShortcodeFor, getEmojiUrl, isUsingTwemoji } from './emoji';
@ -44,7 +49,8 @@ export const LINKIFY_OPTS: LinkifyOpts = {
}; };
export const makeMentionCustomProps = ( export const makeMentionCustomProps = (
handleMentionClick?: ReactEventHandler<HTMLElement> handleMentionClick?: ReactEventHandler<HTMLElement>,
content?: string
): ComponentPropsWithoutRef<'a'> => ({ ): ComponentPropsWithoutRef<'a'> => ({
style: { cursor: 'pointer' }, style: { cursor: 'pointer' },
target: '_blank', target: '_blank',
@ -53,6 +59,7 @@ export const makeMentionCustomProps = (
tabIndex: handleMentionClick ? 0 : -1, tabIndex: handleMentionClick ? 0 : -1,
onKeyDown: handleMentionClick ? onEnterOrSpace(handleMentionClick) : undefined, onKeyDown: handleMentionClick ? onEnterOrSpace(handleMentionClick) : undefined,
onClick: handleMentionClick, onClick: handleMentionClick,
children: content,
}); });
export const renderMatrixMention = ( export const renderMatrixMention = (
@ -86,6 +93,8 @@ export const renderMatrixMention = (
isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
); );
const fallbackContent = mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias;
return ( return (
<a <a
href={href} href={href}
@ -96,7 +105,7 @@ export const renderMatrixMention = (
data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias} data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
data-mention-via={viaServers?.join(',')} data-mention-via={viaServers?.join(',')}
> >
{mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias} {customProps.children ? customProps.children : fallbackContent}
</a> </a>
); );
} }
@ -119,7 +128,9 @@ export const renderMatrixMention = (
data-mention-event-id={eventId} data-mention-event-id={eventId}
data-mention-via={viaServers?.join(',')} data-mention-via={viaServers?.join(',')}
> >
Message: {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias} {customProps.children
? customProps.children
: `Message: ${mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}`}
</a> </a>
); );
} }
@ -201,6 +212,7 @@ export const getReactCustomHtmlParser = (
highlightRegex?: RegExp; highlightRegex?: RegExp;
handleSpoilerClick?: ReactEventHandler<HTMLElement>; handleSpoilerClick?: ReactEventHandler<HTMLElement>;
handleMentionClick?: ReactEventHandler<HTMLElement>; handleMentionClick?: ReactEventHandler<HTMLElement>;
useAuthentication?: boolean;
} }
): HTMLReactParserOptions => { ): HTMLReactParserOptions => {
const opts: HTMLReactParserOptions = { const opts: HTMLReactParserOptions = {
@ -336,12 +348,17 @@ export const getReactCustomHtmlParser = (
} }
if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) { if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) {
const content = children.find((child) => !(child instanceof DOMText))
? undefined
: children.map((c) => (c instanceof DOMText ? c.data : '')).join();
const mention = renderMatrixMention( const mention = renderMatrixMention(
mx, mx,
roomId, roomId,
tryDecodeURIComponent(props.href), tryDecodeURIComponent(props.href),
makeMentionCustomProps(params.handleMentionClick) makeMentionCustomProps(params.handleMentionClick, content)
); );
if (mention) return mention; if (mention) return mention;
} }
@ -363,7 +380,7 @@ export const getReactCustomHtmlParser = (
} }
if (name === 'img') { if (name === 'img') {
const htmlSrc = mx.mxcUrlToHttp(props.src); const htmlSrc = mxcUrlToHttp(mx, props.src, params.useAuthentication);
if (htmlSrc && props.src.startsWith('mxc://') === false) { if (htmlSrc && props.src.startsWith('mxc://') === false) {
return ( return (
<a href={htmlSrc} target="_blank" rel="noreferrer noopener"> <a href={htmlSrc} target="_blank" rel="noreferrer noopener">

View file

@ -253,3 +253,40 @@ export const removeRoomIdFromMDirect = async (mx: MatrixClient, roomId: string):
await mx.setAccountData(AccountDataEvent.Direct, userIdToRoomIds); await mx.setAccountData(AccountDataEvent.Direct, userIdToRoomIds);
}; };
export const mxcUrlToHttp = (
mx: MatrixClient,
mxcUrl: string,
useAuthentication?: boolean,
width?: number,
height?: number,
resizeMethod?: string,
allowDirectLinks?: boolean,
allowRedirects?: boolean
): string | null =>
mx.mxcUrlToHttp(
mxcUrl,
width,
height,
resizeMethod,
allowDirectLinks,
allowRedirects,
useAuthentication
);
export const downloadMedia = async (src: string): Promise<Blob> => {
// this request is authenticated by service worker
const res = await fetch(src, { method: 'GET' });
const blob = await res.blob();
return blob;
};
export const downloadEncryptedMedia = async (
src: string,
decryptContent: (buf: ArrayBuffer) => Promise<Blob>
): Promise<Blob> => {
const encryptedContent = await downloadMedia(src);
const decryptedContent = await decryptContent(await encryptedContent.arrayBuffer());
return decryptedContent;
};

View file

@ -273,16 +273,26 @@ export const joinRuleToIconSrc = (
export const getRoomAvatarUrl = ( export const getRoomAvatarUrl = (
mx: MatrixClient, mx: MatrixClient,
room: Room, room: Room,
size: 32 | 96 = 32 size: 32 | 96 = 32,
): string | undefined => room.getAvatarUrl(mx.baseUrl, size, size, 'crop') ?? undefined; useAuthentication = false
): string | undefined => {
const mxcUrl = room.getMxcAvatarUrl();
return mxcUrl
? mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined
: undefined;
};
export const getDirectRoomAvatarUrl = ( export const getDirectRoomAvatarUrl = (
mx: MatrixClient, mx: MatrixClient,
room: Room, room: Room,
size: 32 | 96 = 32 size: 32 | 96 = 32,
): string | undefined => useAuthentication = false
room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, size, size, 'crop', undefined, false) ?? ): string | undefined => {
undefined; const mxcUrl = room.getAvatarFallbackMember()?.getMxcAvatarUrl();
return mxcUrl
? mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined
: undefined;
};
export const trimReplyFromBody = (body: string): string => { export const trimReplyFromBody = (body: string): string => {
const match = body.match(/^> <.+?> .+\n(>.*\n)*?\n/m); const match = body.match(/^> <.+?> .+\n(>.*\n)*?\n/m);

View file

@ -244,11 +244,7 @@ async function unignore(mx, userIds) {
} }
async function setPowerLevel(mx, roomId, userId, powerLevel) { async function setPowerLevel(mx, roomId, userId, powerLevel) {
const room = mx.getRoom(roomId); const result = await mx.setPowerLevel(roomId, userId, powerLevel);
const powerlevelEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
const result = await mx.setPowerLevel(roomId, userId, powerLevel, powerlevelEvent);
return result; return result;
} }

View file

@ -23,7 +23,6 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
localStorage: global.localStorage, localStorage: global.localStorage,
dbName: 'web-sync-store', dbName: 'web-sync-store',
}); });
await indexedDBStore.startup();
const mx = createClient({ const mx = createClient({
baseUrl: session.baseUrl, baseUrl: session.baseUrl,
@ -38,6 +37,7 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
}); });
await mx.initCrypto(); await mx.initCrypto();
await indexedDBStore.startup();
mx.setGlobalErrorOnUnknownDevices(false); mx.setGlobalErrorOnUnknownDevices(false);
mx.setMaxListeners(50); mx.setMaxListeners(50);

View file

@ -1,5 +1,5 @@
const cons = { const cons = {
version: '4.1.0', version: '4.2.3',
secretKey: { secretKey: {
ACCESS_TOKEN: 'cinny_access_token', ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id', DEVICE_ID: 'cinny_device_id',

View file

@ -5,7 +5,7 @@ export const onLightFontWeight = createTheme(config.fontWeight, {
W100: '100', W100: '100',
W200: '200', W200: '200',
W300: '300', W300: '300',
W400: '420', W400: '400',
W500: '500', W500: '500',
W600: '600', W600: '600',
W700: '700', W700: '700',
@ -17,10 +17,10 @@ export const onDarkFontWeight = createTheme(config.fontWeight, {
W100: '100', W100: '100',
W200: '200', W200: '200',
W300: '300', W300: '300',
W400: '350', W400: '400',
W500: '450', W500: '500',
W600: '550', W600: '600',
W700: '650', W700: '700',
W800: '750', W800: '800',
W900: '850', W900: '900',
}); });

View file

@ -12,6 +12,7 @@ import './index.scss';
import settings from './client/state/settings'; import settings from './client/state/settings';
import { trimTrailingSlash } from './app/utils/common';
import App from './app/pages/App'; import App from './app/pages/App';
// import i18n (needs to be bundled ;)) // import i18n (needs to be bundled ;))
@ -20,6 +21,26 @@ import './app/i18n';
document.body.classList.add(configClass, varsClass); document.body.classList.add(configClass, varsClass);
settings.applyTheme(); settings.applyTheme();
// Register Service Worker
if ('serviceWorker' in navigator) {
const swUrl =
import.meta.env.MODE === 'production'
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
navigator.serviceWorker.register(swUrl);
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'token' && event.data?.responseKey) {
// Get the token for SW.
const token = localStorage.getItem('cinny_access_token') ?? undefined;
event.source!.postMessage({
responseKey: event.data.responseKey,
token,
});
}
});
}
const mountApp = () => { const mountApp = () => {
const rootContainer = document.getElementById('root'); const rootContainer = document.getElementById('root');

52
src/sw.ts Normal file
View file

@ -0,0 +1,52 @@
/// <reference lib="WebWorker" />
export type {};
declare const self: ServiceWorkerGlobalScope;
async function askForAccessToken(client: Client): Promise<string | undefined> {
return new Promise((resolve) => {
const responseKey = Math.random().toString(36);
const listener = (event: ExtendableMessageEvent) => {
if (event.data.responseKey !== responseKey) return;
resolve(event.data.token);
self.removeEventListener('message', listener);
};
self.addEventListener('message', listener);
client.postMessage({ responseKey, type: 'token' });
});
}
function fetchConfig(token?: string): RequestInit | undefined {
if (!token) return undefined;
return {
headers: {
Authorization: `Bearer ${token}`,
},
cache: 'default',
};
}
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(clients.claim());
});
self.addEventListener('fetch', (event: FetchEvent) => {
const { url, method } = event.request;
if (method !== 'GET') return;
if (
!url.includes('/_matrix/client/v1/media/download') &&
!url.includes('/_matrix/client/v1/media/thumbnail')
) {
return;
}
event.respondWith(
(async (): Promise<Response> => {
const client = await self.clients.get(event.clientId);
let token: string | undefined;
if (client) token = await askForAccessToken(client);
return fetch(url, fetchConfig(token));
})()
);
});

View file

@ -10,7 +10,8 @@
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"outDir": "dist", "outDir": "dist",
"skipLibCheck": true "skipLibCheck": true,
"lib": ["ES2016", "DOM"]
}, },
"exclude": ["node_modules", "dist"], "exclude": ["node_modules", "dist"],
"include": ["src"] "include": ["src"]

View file

@ -6,6 +6,7 @@ import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import inject from '@rollup/plugin-inject'; import inject from '@rollup/plugin-inject';
import topLevelAwait from 'vite-plugin-top-level-await'; import topLevelAwait from 'vite-plugin-top-level-await';
import { VitePWA } from 'vite-plugin-pwa';
import buildConfig from './build.config'; import buildConfig from './build.config';
const copyFiles = { const copyFiles = {
@ -58,11 +59,11 @@ export default defineConfig({
port: 8080, port: 8080,
host: true, host: true,
proxy: { proxy: {
"^\\/.*?\\/olm\\.wasm$": { '^\\/.*?\\/olm\\.wasm$': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
rewrite: () => '/olm.wasm' rewrite: () => '/olm.wasm',
} },
} },
}, },
plugins: [ plugins: [
topLevelAwait({ topLevelAwait({
@ -75,6 +76,20 @@ export default defineConfig({
vanillaExtractPlugin(), vanillaExtractPlugin(),
wasm(), wasm(),
react(), react(),
VitePWA({
srcDir: 'src',
filename: 'sw.ts',
strategies: 'injectManifest',
injectRegister: false,
manifest: false,
injectManifest: {
injectionPoint: undefined,
},
devOptions: {
enabled: true,
type: 'module'
}
}),
], ],
optimizeDeps: { optimizeDeps: {
esbuildOptions: { esbuildOptions: {