Compare commits

..

88 Commits
v4.10.2 ... dev

Author SHA1 Message Date
dependabot[bot]
a33e8db9a3 chore(deps): bump dawidd6/action-download-artifact from 16 to 20 (#2880)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 16 to 20.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](2536c51d3d...8305c0f106)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '20'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:36:52 +10:00
dependabot[bot]
fb76e3ecb4 chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#2893)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](bbbca2ddaa...043fb46d1a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  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>
2026-04-14 14:33:37 +10:00
dependabot[bot]
3d79293167 chore(deps): bump softprops/action-gh-release from 2.3.3 to 3.0.0 (#2892)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.3 to 3.0.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](6cbd405e2c...b430933298)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:33:01 +10:00
dependabot[bot]
74745edcda chore(deps): bump nginx from 1.29.5-alpine to 1.29.8-alpine (#2894)
Bumps nginx from 1.29.5-alpine to 1.29.8-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-version: 1.29.8-alpine
  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>
2026-04-14 14:31:53 +10:00
dependabot[bot]
0812131a97 chore(deps): bump docker/build-push-action from 6.19.2 to 7.1.0 (#2895)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](10e90e3645...bcafcacb16)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:27:58 +10:00
dependabot[bot]
1068bba5c7 chore(deps): bump docker/login-action from 3.7.0 to 4.1.0 (#2879)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c94ce9fb46...4907a6ddec)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:26:52 +10:00
DJ Chase
1b5e58a3b4 chore: add matrixrooms.info to directory list (#2844)
* chore: add matrixrooms.info to directory list

matrixrooms.info is a directory of all public Matrix rooms it can find,
regardless of homeserver. It is much larger than the morg directory,
so is more useful as a general search
2026-03-28 17:35:53 +11:00
Krishan
acae043f31 chore: make error more useful and understandable (#2859)
* chore: make error more useful and understandable

* chore: use similar wording
2026-03-27 21:22:46 +11:00
ranidspace
b4299f8f37 feat: add YYYY-MM-DD (ISO 8601) date format to presets (#2712)
* Add YYYY-MM-DD (ISO 8601) date format to presets

* Fix formatting due to added date format
2026-03-27 21:20:10 +11:00
Krishan
b6adac6714 chore: add notice about SDK replacement (#2778) 2026-03-25 12:10:15 +11:00
DJ Chase
1c8f203164 chore: add 'Stickers and Emojis' as featured space (#2842)
* Mention CLA in CONTRIBUTING.md

Closes: #2146

* add: 'Stickers and Emojis' to config.json

Add #stickers-and-emojis:tastytea.de (space) to config.json
2026-03-25 12:09:16 +11:00
Krishan
0c30ece281 fix: remove typo in no rooms UI (#2834) 2026-03-23 16:57:52 +11:00
Krishan
4e559e56d4 chore: group related package update together (#2833) 2026-03-23 16:49:22 +11:00
Krishan
19f28b40ac chore: use private vulnerability disclosure (#2827) 2026-03-22 18:29:09 +11:00
Krishan
bcaf43a540 chore: fix link in issue triage template (#2826)
* chore: fix link in issue triage template

* chore: delete .github/PULL_REQUEST_TEMPLATE.md
2026-03-22 18:20:33 +11:00
Krishan
9c7b635e7e chore: add new issue triage discussion template (#2825)
* chore: add new issue triage discussion template

* chore: ask for desktop app version as well

* chore: create preapproved.md
2026-03-22 17:55:53 +11:00
Krishan
65c87dff3a chore: add git author to the sem release (#2815) 2026-03-21 12:07:52 +11:00
Krishan
132a76df27 chore: add semantic release (#2759)
* chore: install deps related to semantic release

* chore: add husky config

* ci: add a script to update version number on new release

* ci: update ci/cd to include semantic release changes

* chore: merge dev to semantic-release
2026-03-19 16:26:25 +11:00
DJ Chase
b0954eeddc fix: Mention CLA in CONTRIBUTING.md (#2804)
Mention CLA in CONTRIBUTING.md

Closes: #2146
2026-03-19 11:41:41 +11:00
Ajay Bura
8f1add6059 fix: prevent codeblock filename drop on edit (#2780)
prevent codeblock filename drop on edit
2026-03-15 15:37:14 +11:00
Jan Jurzitza
8a78c9699e feat: allow using filenames in codeblocks (#2455)
Allow using filenames in codeblocks

- If there is a dot in the language name, we instead treat the first line after ``` as the filename and everything after the last dot as the language
- we use a custom "data-label" attribute on the code block to specify the name of the file (so only compatible with cinny from this point onwards)
2026-03-14 18:54:03 +11:00
Krishan
0721b29a2c chore: batch slate related deps (#2775) 2026-03-14 17:22:57 +11:00
Ajay Bura
3d354909d6 fix: hover state on url preview image and make it keyboard friendly (#2777)
add hover state on url preview image and make it keyboard friendly
2026-03-14 17:22:18 +11:00
LeaPhant
7570a84dfd Show image viewer when clicking url preview thumbnail (#2309)
* Show large image overlay when clicking url preview thumbnail

* Move image overlay into its own component

* Move ImageOverlay props into extended type

* Remove export for internal type
2026-03-14 11:04:55 +05:30
Krishan
6a05ff5840 chore(release): v4.11.1 [skip ci] (#2765)
* chore(release): 4.11.0 [skip ci]

* chore(release): 4.11.1 [skip ci]
2026-03-11 23:07:37 +11:00
Krishan
919fe8381b chore(deps): Update slate deps to 0.123.0 (#2764)
Update slate deps to latest
2026-03-11 23:06:53 +11:00
Krishan
b76ad3caaf chore(release): 4.11.0 [skip ci] 2026-03-11 04:03:48 +00:00
Ajay Bura
409d45857d chore(deps): update folds to 2.6.2 (#2762)
update folds to 2.6.2
2026-03-10 23:03:03 +11:00
Ajay Bura
0d1566977a fix: call ui imorovements (#2749)
* fix member button tooltip in call room header

* hide sticker button in room input based on chat window width

* render camera on off data instead of duplicate join messages

* hide duplicate call member changes instead of rendering as video status

* fix prescreen message spacing
2026-03-10 22:45:26 +11:00
Krishan
0cbfbab5ad chore: add semantic commits to renovate configuration (#2760)
* Add semantic commits to Renovate configuration

* Fix formatting issue in renovate.json
2026-03-10 14:57:20 +11:00
Krishan
37e0c2aaac chore(deps): continue action if login fails (#2758)
chore(action): continue action if login fails
2026-03-10 12:26:55 +11:00
Krishan
296249de32 chore: enable semantic check on PR title (#2447)
* Enable semantic check on PR title

* Update semantic pull request action version

* Update PR trigger types in workflow configuration

Removed 'reopened' and 'synchronize' types from pull request triggers.
2026-03-10 12:26:25 +11:00
Ajay Bura
4449e7c6e8 Show call support error and disable join button (#2748)
* allow user to end call if error when loading

* show call support missing error if livekit server is not provided

* prevent joining from nav item double click if no livekit support
2026-03-09 21:39:58 +11:00
Ajay Bura
2eb5a9a616 Fix crash with bad location uri (#2746)
fix crash with bad location uri
2026-03-09 18:17:15 +11:00
Ajay Bura
d679e68501 Fix recent emoji does not persist (#2722)
Fix recent emoji are not getting saved

Refactor recent emoji retrieval to ensure structured cloning and proper type checking. The sdk was not updating account data because we are mutating the original and it compare and early return if found same.
2026-03-09 17:34:44 +11:00
Ajay Bura
bc6caddcc8 Add own control buttons for element-call (#2744)
* add mutation observer hok

* add hook to read speaking member by observing iframe content

* display speaking member name in call status bar and improve layout

* fix shrining

* add joined call control bar

* remove chat toggle from room header

* change member speaking icon to mic

* fix joined call control appear in other

* show spinner on end call button

* hide call statusbar for mobile view when room is selected

* make call statusbar more mobile friendly

* fix call status bar item align
2026-03-09 08:34:48 +05:30
Ajay Bura
55e8306576 Display call member speaking status on bottom bar (#2742)
* add mutation observer hok

* add hook to read speaking member by observing iframe content

* display speaking member name in call status bar and improve layout

* fix shrining
2026-03-08 22:00:35 +11:00
Ajay Bura
7953ec80e5 Apply deafen state when call member changes (#2737)
* fix deafen not working

* apply deafen state when call member changes

* remove unnecessary condition
2026-03-08 14:22:11 +11:00
Ajay Bura
c6bb4915bc Downgrade matrix-widget-api from 1.17.0 to 1.13.0 (#2736)
Some user was having Disconnection Error
2026-03-07 20:47:45 +11:00
Krishan
b050cd01f9 feat: Add voice/video room support (#2680)
* Add users on the nav to showcase call activity and who is in the call

* add check to prevent DCing from the call you're currently in...

* Add avatar and username for the space (needs to be moved into RoomNavItem proper)

* Add background variant to buttons

* Update hook to keep method signature (accepting an array of Rooms instead) to support multiple room event tracking of the same event

* Add state listener so the call activity is real time updated on joins/leaves within the space

* Add RoomNavUser for displaying the user avatar + name in the nav for a visual of call activity and participants

* rename CallNavBottom to CallNavStatus

* Rename callnavbottom and fix linking implementation to actually be correct

* temp fix to allow the status to be cleared in some way

* re-add background to active call link button

* prepare to feed this to child elements for visibility handling

* loosely provide nav handling for testing refactoring

* Add CallView

* Update to funnel Outlet context through for Call handling (might not be the best approach, but removes code replication in PersistentCallContainer where we were remaking the roomview entirely)

* update client layout to funnel outlet the iframes for the call container

* funnel through just iframe for now for testing sake

* Update room to use CallView

* Pass forward the backupIframeRef now

* remove unused params

* Add backupIframeRef so we can re-add the lobby screen for non-joined calls (for viewing their text channels)

* Remove unused imports and restructure to support being parent to clientlayout

* Re-add layout as we're no longer oddly passing outlet context

* swap to using ref provider context from to connect to persistentcallcontainer more directly

* Revert to original code as we've moved calling to be more inline with design

* Revert to original code as we've moved the outlet context passing out and made more direct use of the ref

* Fix unexpected visibility in non-room areas

* correctly provide visibility

* re-add mobile chat handling

* Improve call room view stability

* split into two refs

* add ViewedRoom usage

* Disable

* add roomViewId and related

* (broken) juggle the iframe states proper... still needs fixing

* Conditionals to manage the active iframe state better

* add navigateRoom to be in both conditions for the nav button

* Fix the view to correctly display the active iframe based on which is currently hosting the active call (juggling views)

* Testing the iframe juggling. Seems to work for the first and second joins... so likely on the right path with this

* add url as a param for widget url

* fix backup iframe visibility

* Much closer to the call state handling we want w/ hangups and joins

* Fix the position of the member drawer to its correct location

* Ensure drawer doesn't appear in call room

* Better handling of the isCallActive in the join handler

* Add ideal call room join behavior where text rooms to call room simply joins, but doesn't swap current view

* Fix mobile call room default behavior from auto-join to displaying lobby

* swap call status to be bound to call state and not active call id

* Remove clean room ID and add default handler for if no active call has existed yet, but user clicks on show chat

* Applies the correct changes to the call state and removes listeners of old active widget so we don't trigger hang ups on the new one (the element-call widget likes to spam the hang up response back several times for some reason long after you tell it to hang up)

* Remove superfluous comments and Date.now() that was causing loading... bug when widgetId desynced

* Remove Date.now() that was causing widgetId desync

* add listener clearing, camel case es lint rule exception, remove unneeded else statements

* Remove unused

* Add widgetId as a getWidgetUrl param

* Remove no longer needed files

* revert ternary expression change and add to dependency array

* add widgetId to correct pos in getWidgetUrl usage

* Remove CallActivation

* Move and rename RoomCallNavStatus

* update imports and dependency array

* Rename and clean up

* Moved CallProvider

* Fix spelling mistake

* Fix to use shorthand prop

* Remove unneeded logger.errors

* Fixes element-call embedded support (but it seems to run poorly)

* null the default url so that we fallback to the embedded version (would recommend hosting it until performance issue is determined)

* Fix vite build to place element-call correctly for embedded npm package support

* add vite preview as an npm script

* Move files to more correct location

* Add package-lock changes

* Set dep version to exact

* Fix path issue from moving file locations

* Sets initial states so the iframes don't cause the other to fail with the npm embedded package

* Revert navitem change

* Just check for state on both which should only occur at initial

* Fixes call initializing by default on mobile

* Provides correct behavior when call isn't active and no activeClientWidgetApi exists yet

* Corrects the state for the situations where both iframes are "active" (not necessarily visible)

* Reduce code reuse in handleJoin

* Seems to sort out the hangup status button bug the occurred after joining a call via lobby

* Re-add the default view current active room behavior

* Remove repetitive check

* Add storing widget for comparing with (since we already store room id and the clientWidgetApi anyway)

* Update rendering logic to clear up remaining rendering bug (straight to call -> lobby of another room and joining call from that interface -> lobby of that previous room and joining was leading to duplication of the user in lobbies. This was actually from listening to and acknowledging hangups from the viewed widget in CallProvider)

* Prevent null rooms from ever rendering

* This seems to manage the hangup state with the status bar button well enough that black screens should never be encountered

* Remove viewed room setting here and pass the room to hang up (seems state doesn't update fast enough otherwise)

* Remove unused

* Properly declare new hangup method sig

* Seems to avoid almost all invalid states (hang up while viewing another lobby and hitting join seems to black screen, sets the active call as the previous active room id, but does join the viewed room correctly)

* Fix for cases where you're viewing a lobby and hang up your existing call and try to join lobby (was not rendering for the correct room id, but was joining the correct call prior)

* Re-add intended switching behavior

* More correct filter (viewedRoom can return false on that compare in some cases)

* Seems to shore up the remaining state issues with the status bar hangup

* Fix formatting

* In widget hang up button should be handled correct now

* Solves the CHCH sequence issue, CLJH remains

* Fixes CLJH, found CCH

* Solves CCH. Looks like CLCH left

* A bit of an abomination, but adds a state counter to iteratively handle the diverse potential states (where a user can join from the nav bar or the join button, hang up from either as well, and account for the juggling iframes)
Black screens shouldn't be occurring now.

* Fix dependency array

* Technically corrects the hangup button in the widget, should be more precise though

* Bind the on messaging iframe for easier access in hangup/join handling

* Far cleaner and more sensible handling of the call window... I just really don't like the idea of sending a click event, but right now the element-call code treats preload/skipLobby hangups (sent from our end) as if they had no lobby at all and thus black screens. Other implementation was working around that without just sending a click event on the iframe's hangup button.

* Fixes a bug where if you left a call then went to a lobby and joined it didn't update the actual activeCallRoomId

* Fixes complaints of null contentDocument in iframe

* Update to use new icons (thank you)

* Remove unneeded prop

* Re-arrange more options and add checks for each option to see if it is a call room (probably should manage a state to see if a header is already on screen and provide a slightly modified visual based on that for call rooms)

* Invert icons to show the state instead of the action they will perform (more visual clarity)

* Update src/app/features/room-nav/RoomCallNavStatus.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomCallNavStatus.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomCallNavStatus.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomCallNavStatus.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomCallNavStatus.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomCallNavStatus.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomViewHeader.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavItem.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavUser.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavUser.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room-nav/RoomNavUser.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/pages/client/space/Space.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/call/CallView.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/call/CallView.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/call/CallView.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomView.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomView.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* adjust room header for calling

* Remove No Active Call text when not in a call

* update element-call version

* Update src/app/features/room/RoomViewHeader.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomViewHeader.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomViewHeader.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomViewHeader.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Update src/app/features/room/RoomViewHeader.tsx

Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>

* Revert most changes to Space.tsx

* Show call room even if category is collapsed

* changes to RoomNavItem, RoomNavUser and add useCallMembers

* Rename file, sprinkle in the magic one line for matrixRTCSession. and remove comment block

* swap userId to callMembership as a prop and add a nullchecked userId that uses the membership sender

* update references to use callMembership instead

* Simplify RoomNavUser

Discard future functionality since it probably won't exist in time for merging this PR

* Simplify RoomViewHeader.tsx

Remove unused UI elements that don't have implemented functionality. Replace custom function for checking if a room is direct with a standard hook.

* Update Room.tsx to accomodate restructuring of Room, RoomView and CallView

* Update RoomView.tsx to accomodate restructuring of Room, RoomView and CallView

* Update CallView.tsx to accomodate restructuring of Room, RoomView and CallView + suggested changes

* add call related permissions to room permissions

* bump element call to 0.16.3, apply cinny theme to element call ui, replace element call lobby (backup iframe) with custom ui and only use element call for the in-call ui

* update text spacing

* redo roomcallnavstatus ui, force user preferred mute/video states when first joining calls, update variable names and remove unnecessary logic

* set default mic state to enabled

* clean up ts/eslint errors

* remove debug logs

* format using prettier rules from project prettierrc

* fix: show call nav status while active call is ongoing

* fix: clean up call nav/call view console warnings

* fix: keep call media controls visible before joining

* fix: restore header icon button fill behavior

Fixes regression from b074d421b66eb4d8b600dfa55b967e6c4f783044.

* style: blend header and room input button styles in call nav

* fix page header background color on room view header

* fix: permissions and room icon resolution (#2)

* Initialize call state upon room creation for call rooms, remove subsequent useless permission

* handle case of missing call permissions

* use call icon for room item summary when room is call room

* replace previous icon src resolution function with a more robust approach

* replace usages of previous icon resolution function with new implementation

* fix room name not updating for a while when changed

* set up framework for room power level overrides upon room creation

* override join call permission to all members upon room creation

* fix broken usages of RoomIcon

* remove unneeded import

* remove unnecessary logic

* format with prettier

* feat: show connected/connecting call status

* fix: preserve navigation context when opening non-call rooms

* fix: reset room name state when room instance changes

* feat: Disable webcam by default using callIntent='audio'

* Add channel type selecor

* Add option for voice rooms, which for now sets the default selected
option in the creation modal

* Add proper support for room selection from the enu

* Move enums to `types.ts` and change icons selection to use
`getRoomIconSrc`

* fix: group duplicate conditions into one

* fix: typo

* refactor: rename kind/voice to access/type and simplify room creation

- rename CreateRoomVoice to CreateRoomType and modal voice state to type
- rename CreateRoomKind to CreateRoomAccess and KindSelector to AccessSelector
- propagate access/defaultAccess through create room and create space forms
- set voice room power levels via createRoom power_level_content_override

* refactor: unify join rule icon mapping and update call/space icons

- bump folds from 2.5.0 to 2.6.0
- replace separate room/space join-rule icon hooks with useJoinRuleIcons(roomType)
- route join-rule icons through getRoomIconSrc for consistent room type handling
- simplify getRoomIconSrc by removing the locked override path
- use VolumeHighGlobe for public call rooms and VolumeHighLock for private call rooms

* chore(deps): bump matrix-widget-api to 1.17 and remove react-sdk-module-api

* fix: adapt SmallWidget to matrix-widget-api 1.17.0 API

* fix: render call room chat only when chat panel is open

* fix(permissions): show call settings permissions only for call rooms

* refactor: remove redundant room-nav props/guards and minor naming cleanup

* fix: use PhoneDown icon for hang up action

* chore(hooks): remove unused useStateEvents hook

* fix(room): enable members drawer toggle in desktop call rooms

- show filled User icon when the drawer is open

* Revert "fix: adapt SmallWidget to matrix-widget-api 1.17.0 API"

This reverts commit a4c34eff8a.

* fix: semi-revert matrix-widget-api 1.17 bump and migrate to 1.13 API

* fix(call): wait for Element Call contentLoaded before widget handshake

- fixes not working on firefox

* fix missing imports

* improve create room type design and add beta badge for voice room

* add beta badge for voice room in space lobby

* fix create room modal title

* pass missing roomType param to roomicon component

* add roomtype

* Add deafen functionality (#2695)

* feat:(deafen functionality)

* feat:(reworked voice controls for deafen)

* ref:(use muted instead of volume for deafen)

* fix:(backpedal audio_enabled rename)

* ref:(renaming of deafened vars)

* add stack avatar component

* add call status bar - WIP

* remove call status from navigation drawer

* fix deprecated method use in use call members hook

* render new call status bar

* move call widget driver to plugins

* remove old status bar usage from navigation drawer

* add call session and joined hook

* remove unknown changes

* upgrade widget api

* add element call embed plugin

* remove unknown change

* add call embed atom

* add call embed hooks and context

* add call embed provider

* replace old call implementation

* stop joining other call on second click if already in a call

* refactor embed placement hook

* add merge border prop to sequence card

* add call preferences

* add prescreen to call view - WIP

* prevent joining new call if already in call

* make call layout adaptive

* render call chat as right panel

* show call members in prescreen

* render call join leave event in timeline

* remove unknown rewrite in docker-nginx file

* render call event without hidden event enable

---------

Co-authored-by: Gigiaj <gigiaboone@yahoo.com>
Co-authored-by: Jaggar <18173108+GigiaJ@users.noreply.github.com>
Co-authored-by: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com>
Co-authored-by: Gimle Larpes <gimlelarpes@gmail.com>
Co-authored-by: YoJames2019 <jamesclark1700@gmail.com>
Co-authored-by: YoJames2019 <yobiscuit0@gmail.com>
Co-authored-by: hazre <mail@haz.re>
Co-authored-by: haz <37149950+hazre@users.noreply.github.com>
Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Co-authored-by: James <49845975+YoJames2019@users.noreply.github.com>
Co-authored-by: James Reilly <jreilly1821@gmail.com>
Co-authored-by: Tymek <vonautymek@gmail.com>
Co-authored-by: Thedustbuster <92692948+Thedustbustr@users.noreply.github.com>
2026-03-07 18:03:32 +11:00
renovate[bot]
730670cf52 chore(deps): update actions/setup-node action to v6.3.0 (#2727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 20:10:28 +11:00
Krishan
c5c8703699 Update deploy PR workflow name and run name (#2728) 2026-03-04 20:07:16 +11:00
Krishan
2bd1570d6b Update deploy PR workflow name to include PR number (#2726) 2026-03-04 19:30:36 +11:00
dependabot[bot]
68b6a09697 Bump dawidd6/action-download-artifact from 15 to 16 (#2719)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 15 to 16.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](fe9d59ce33...2536c51d3d)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '16'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 19:08:23 +11:00
dependabot[bot]
7b52c921d5 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#2718)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6.0.0...v7.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 18:50:22 +11:00
Krishan
bb8b9ab6da Add more docker related action checks (#2724)
* Pin all the action deps to SHA

* Add more docker related action checks

* Limit Docker build platforms to linux/amd64

Updated Docker build action to target only linux/amd64 platform.
2026-03-04 18:31:54 +11:00
Krishan
971f312b46 Pin all the action deps to SHA (#2725) 2026-03-04 12:31:36 +11:00
Tulir Asokan
e0d5c63dc5 Fix invalid matrix.to event link generation (#2717) 2026-03-03 20:12:45 +11:00
renovate[bot]
85fcbd84fe chore(deps): update thollander/actions-comment-pull-request from 2.5.0 to 3.0.1 (#2698)
* chore(deps): update thollander/actions-comment-pull-request digest to e4a76dd

* pin to v3.0.1

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
2026-02-24 18:29:40 +11:00
dependabot[bot]
221bc04754 Bump dawidd6/action-download-artifact from 11 to 15 (#2694)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 11 to 15.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](ac66b43f0e...fe9d59ce33)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '15'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 17:54:57 +11:00
Krishan
6347640a35 Release v4.10.5 (#2692) 2026-02-23 23:05:01 +11:00
renovate[bot]
f2d8ad0b6b chore(deps): update docker/metadata-action action to v5.10.0 (#2690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:58:20 +11:00
renovate[bot]
739786d9ab chore(deps): update docker/setup-qemu-action action to v3.7.0 (#2691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:57:44 +11:00
renovate[bot]
f642809939 chore(deps): update docker/login-action action to v3.7.0 (#2689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:56:52 +11:00
Krishan
02106a99b9 Release v4.10.4 (#2688) 2026-02-23 22:32:06 +11:00
dependabot[bot]
df3a3ba789 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#2641)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.11.1...v3.12.0)

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

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

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

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

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

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

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

* update linkify react

---------

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

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

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

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

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

* Fix dockerfile node version

* Simplify node and nginx version, bump nginx

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

* fix async session request in fetch

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

* make sure we call respondWith synchronously

* simplify isMediaRequest in sw

* improve naming in sw

* get back baseUrl check into validMediaRequest

* pass original request into fetch in sw

* extract mediaPath util and performs checks properly

---------

Co-authored-by: mmmykhailo <35040944+mmmykhailo@users.noreply.github.com>
2026-02-21 17:51:27 +11:00
Krishan
29ec172c8b Release v4.10.3 (#2608) 2026-02-16 22:19:21 +11:00
Rin
0f220f50d6 fix: add noreferrer to sanitized links for improved privacy consistency (#2628)
Enhance privacy by adding noreferrer to sanitized links
2026-02-16 19:54:05 +11:00
Ajay Bura
d866c1b903 fix room back button not working after router update (#2630) 2026-02-16 19:51:55 +11:00
Ajay Bura
fbde1a2030 fix: image not loading on mobile after lock/unlock (#2631)
image not loading on mobile after lock/unlock
2026-02-16 19:51:09 +11:00
Krishan
4ba7b9162d Revert "fix: set m.fully_read marker when marking rooms as read" (#2629)
Revert "Set m.fully_read marker when marking rooms as read (#2587)"

This reverts commit 9d49418a1f.
2026-02-16 06:03:37 +11:00
Andrew Murphy
9d49418a1f Set m.fully_read marker when marking rooms as read (#2587)
Previously markAsRead() only sent m.read receipts via sendReadReceipt().
This meant the read position was not persisted across page refreshes,
especially noticeable in bridged rooms.

Now uses setRoomReadMarkers() which sets both:
- m.fully_read marker (persistent read position)
- m.read receipt

Fixes issue where rooms would still show as unread after refresh.
2026-02-14 17:32:10 +11:00
Ajay Bura
3522751a15 Prevent invalid mxc from getting used (#2609) 2026-02-14 17:12:28 +11:00
Ajay Bura
074c555294 Post session info to service worker instead of asking from sw (#2605)
post session info to service worker instead of asking from sw on each request
2026-02-14 17:11:36 +11:00
renovate[bot]
206a927f30 fix(deps): update dependency react-router-dom to v6.30.3 (#2612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 17:10:43 +11:00
Andrew Murphy
fd37dfe3f9 Fix muted rooms showing unread badges (#2581)
fix: detect muted rooms with empty actions array

The mute detection was checking for `actions[0] === "dont_notify"` but
Cinny sets `actions: []` (empty array) when muting a room, which is
the correct behavior per Matrix spec where empty actions means no
notification.

This caused muted rooms to still show unread badges and contribute to
space badge counts.

Fixes the isMutedRule check to handle both:
- Empty actions array (current Matrix spec)
- "dont_notify" string (deprecated but may exist in older rules)
2026-02-12 21:45:37 +11:00
Gimle Larpes
1ce6ca2b07 Re-add mEvent.getSender() === mx.getUserId() check for deletion of messages (#2607)
* hide "Delete Message" if it is forbidden

* Fix the stuff I broke :/
2026-02-12 21:40:11 +11:00
renovate[bot]
83e5125b37 fix(deps): update dependency folds to v2.5.0 (#2606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 16:56:47 +11:00
Gimle Larpes
ca82aa283a Hide "Delete Message" if it is forbidden (#2602)
hide "Delete Message" if it is forbidden
2026-02-12 16:27:17 +11:00
Zach
8ce33ee6ff Replace envs.net with unredacted.org in config (#2601)
* Replace 'envs.net' with 'unredacted.org' in config

https://envs.net/ is shutting down their Matrix server

* Update defaultHomeserver and reorder servers list

* Remove 'monero.social' from homeserver list
2026-02-12 10:39:58 +11:00
Santhoshkumar044
073a9f5786 Fix room alias mention triggering room-wide notifications (#2562)
* fix: prevent room alias mentions from triggering @room notifications

* fix: Simplify room mention to exact match on @room
2026-01-12 23:21:00 +11:00
dependabot[bot]
655c1c9aff Bump docker/login-action from 3.5.0 to 3.6.0 (#2496)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 16:30:39 +11:00
dependabot[bot]
17d4bceb42 Bump nginx from 1.29.1-alpine to 1.29.3-alpine (#2525)
Bumps nginx from 1.29.1-alpine to 1.29.3-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-version: 1.29.3-alpine
  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>
2025-11-27 16:16:20 +11:00
willow
0f61f2f328 Fix typo: change "Advance Options" to "Advanced Options" (#2537) 2025-11-27 16:01:40 +11:00
147 changed files with 11939 additions and 685 deletions

View File

@@ -0,0 +1,127 @@
labels: ["needs-confirmation"]
body:
- type: markdown #add faqs in future
attributes:
value: |
> [!IMPORTANT]
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- type: markdown
attributes:
value: "# Issue Details"
- type: textarea
attributes:
label: Issue Description
description: |
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
placeholder: |
When I try to send a message in a room, the message doesn't appear in the timeline.
OR
The application crashes when I click on the settings button.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: |
Describe how you expect Cinny to behave in this situation.
placeholder: |
I expected the message to appear in the room timeline immediately after sending.
OR
The settings panel should open smoothly without any crashes.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: |
Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
placeholder: |
The application freezes for 3 seconds and then shows a white screen.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Provide a detailed set of step-by-step instructions for reproducing this issue.
placeholder: |
1. Open Cinny and log in to my account
2. Navigate to the #general room
3. Type a message in the message box
4. Press Enter to send
5. Notice that the message doesn't appear in the timeline
validations:
required: true
- type: textarea
attributes:
label: Environement
description: |
Please provide information about your environment. Include the following:
- OS:
- Browser:
- Cinny Web Version: (app.cinny.in or self hosted)
- Cinny desktop Version: (appimage or deb or flatpak)
- Matrix Homeserver:
placeholder: |
- OS: Windows 11
- Browser: Chrome 120.0.6099.109
- Cinny Web Version: 3.2.0 (app.cinny.in or self hosted)
- Cinny desktop Version: 3.2.0 (appimage or deb or flatpak)
- Matrix Homeserver: matrix.org (Synapse 1.97.0)
render: text
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: |
If applicable, add browser console logs to help explain your problem.
**To get browser console logs:**
- Chrome/Edge: Press F12 → Console tab
- Firefox: Press F12 → Console tab
- Safari: Develop → Show Web Inspector → Console
Please wrap large log outputs in code blocks with triple backticks (```).
placeholder: |
```
Error: Failed to send message
at MessageComposer.sendMessage (composer.js:245)
at HTMLButtonElement.onClick (composer.js:189)
TypeError: Cannot read property 'content' of undefined
at RoomTimeline.render (timeline.js:567)
```
render: shell
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Add any other context about the problem here (e.g., when did this start happening, does it happen on different homeservers, etc.)
placeholder: |
- This started happening after I updated to version 3.2.0
- It only happens in encrypted rooms, not in public rooms
- I've tried on both Firefox and Chrome with the same result
- It works fine on my phone using the same account
- This happens on all homeservers I've tested (matrix.org, mozilla.org)
validations:
required: false
- type: markdown
attributes:
value: |
# User Acknowledgements
> [!TIP]
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
- type: checkboxes #add faqs in future
attributes:
label: "I acknowledge that:"
options:
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required: true
- label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
required: true

View File

@@ -1,57 +0,0 @@
name: 🐞 Bug Report
description: Report a bug
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.

View File

@@ -1,4 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers
- name: Features, Bug Reports, Questions
url: https://github.com/cinnyapp/cinny/discussions/new/choose
about: Our preferred starting point if you have any questions or suggestions about features or behavior.

View File

@@ -1,33 +0,0 @@
name: 💡 Feature Request
description: Suggest an idea
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.

9
.github/ISSUE_TEMPLATE/preapproved.md vendored Normal file
View File

@@ -0,0 +1,9 @@
---
name: Pre-Discussed and Approved Topics
about: |-
Only for topics already discussed and approved in the GitHub Discussions section.
---
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**

View File

@@ -1,22 +0,0 @@
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings

3
.github/SECURITY.md vendored
View File

@@ -1,3 +0,0 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**

20
.github/renovate.json vendored
View File

@@ -1,10 +1,28 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":dependencyDashboardApproval"],
"extends": [
"config:recommended",
":dependencyDashboardApproval",
":semanticCommits",
"group:monorepos"
],
"labels": ["Dependencies"],
"rebaseWhen": "conflicted",
"packageRules": [
{
"matchUpdateTypes": ["lockFileMaintenance"]
},
{
"groupName": "Slatejs",
"matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"]
},
{
"groupName": "Call",
"matchPackageNames": ["@element-hq/element-call-embedded", "matrix-widget-api"]
},
{
"groupName": "Linkify",
"matchPackageNames": ["linkifyjs", "linkify-react"]
}
],
"lockFileMaintenance": {

View File

@@ -12,12 +12,12 @@ jobs:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 20.12.2
cache: 'npm'
node-version-file: ".node-version"
package-manager-cache: false
- name: Install dependencies
run: npm ci
- name: Build app
@@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: preview
path: dist
@@ -33,7 +33,7 @@ jobs:
- name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr
path: ./pr.txt

View File

@@ -12,7 +12,7 @@ jobs:
- 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'
# Beta Release
uses: cla-assistant/github-action@v2.6.1
uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret

View File

@@ -1,4 +1,5 @@
name: Deploy PR to Netlify
run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
on:
workflow_run:
@@ -15,7 +16,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download pr number
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -24,7 +25,7 @@ jobs:
id: pr
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
- name: Download artifact
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -32,7 +33,7 @@ jobs:
path: dist
- name: Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
with:
publish-dir: dist
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
@@ -45,12 +46,12 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
timeout-minutes: 1
- name: Comment preview on PR
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b #v3.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
with:
pr_number: ${{ steps.pr.outputs.id }}
comment_tag: ${{ steps.pr.outputs.id }}
pr-number: ${{ steps.pr.outputs.id }}
comment-tag: ${{ steps.pr.outputs.id }}
message: |
Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️

View File

@@ -5,15 +5,59 @@ on:
paths:
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
- '.github/workflows/prod-deploy.yml'
jobs:
docker-build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
- name: Build Docker image
uses: docker/build-push-action@v6.18.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
continue-on-error: true
- name: Login to the Github Container registry #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Extract metadata (tags, labels) for Docker, GHCR
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
ajbura/cinny
ghcr.io/${{ github.repository }}
- name: Build Docker image (no push)
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Show Docker images
run: docker images

View File

@@ -14,9 +14,9 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values.

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 20.12.2
cache: 'npm'
node-version-file: ".node-version"
package-manager-cache: false
- name: Install dependencies
run: npm ci
- name: Build app
@@ -24,7 +24,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
with:
publish-dir: dist
deploy-message: 'Dev deploy ${{ github.sha }}'

15
.github/workflows/pr-title.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Check PR title
on:
pull_request_target:
types:
- opened
- edited
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,32 +1,48 @@
name: Production deploy
on:
release:
types: [published]
workflow_dispatch:
jobs:
deploy-and-tarball:
name: Netlify deploy and tarball
outputs:
version: ${{ steps.vars.outputs.tag }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
- name: Setup node
uses: actions/setup-node@v4.4.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
node-version: 20.12.2
cache: 'npm'
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".node-version"
package-manager-cache: false
- name: Install dependencies
run: npm ci
- name: Run semantic release
run: npm run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: Get version from tag
id: vars
run: |
TAG=$(git describe --tags --abbrev=0)
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Build app
env:
NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
with:
publish-dir: dist
deploy-message: 'Prod deploy ${{ github.ref_name }}'
deploy-message: 'Prod deploy ${{ steps.vars.outputs.tag }}'
enable-commit-comment: false
github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true
@@ -36,9 +52,6 @@ jobs:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
timeout-minutes: 1
- name: Get version from tag
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Sign tar.gz
@@ -52,45 +65,54 @@ jobs:
gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ steps.vars.outputs.tag }}
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
publish-image:
name: Push Docker image to Docker Hub, ghcr
name: Push Docker image to Docker Hub, GHCR
needs: deploy-and-tarball
env:
VERSION: ${{ needs.deploy-and-tarball.outputs.version }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
- name: Login to Docker Hub
uses: docker/login-action@v3.5.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub #Do not update this action from a outside PR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry
uses: docker/login-action@v3.5.0
- name: Login to the Github Container registry #Do not update this action from a outside PR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
- name: Extract metadata (tags, labels) for Docker, GHCR
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ env.VERSION }}
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64

3
.husky/pre-commit Normal file
View File

@@ -0,0 +1,3 @@
# These are commented until we enable lint and typecheck
# npx tsc -p tsconfig.json --noEmit
# npx lint-staged

1
.node-version Normal file
View File

@@ -0,0 +1 @@
24.13.1

View File

@@ -18,7 +18,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
## Pull requests
> ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.

View File

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

View File

@@ -16,6 +16,10 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
- [Contributing](./CONTRIBUTING.md)
> [!IMPORTANT]
We are currently in the [process of replacing the matrix-js-sdk](https://github.com/cinnyapp/cinny/issues/257#issuecomment-3714406704) with our own SDK. As a result, we will not be accepting any pull requests until further notice.
Thank you for your understanding.
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
## Getting started
@@ -83,7 +87,7 @@ mxFo+ioe/ABCufSmyqFye0psX3Sp
## Local development
> [!TIP]
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Iron LTS (v20).
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
Execute the following commands to start a development server:
```sh

View File

@@ -1,13 +1,6 @@
{
"defaultHomeserver": 2,
"homeserverList": [
"converser.eu",
"envs.net",
"matrix.org",
"monero.social",
"mozilla.org",
"xmr.se"
],
"defaultHomeserver": 1,
"homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"],
"allowCustomHomeservers": true,
"featuredCommunities": {
@@ -15,10 +8,11 @@
"spaces": [
"#cinny-space:matrix.org",
"#community:matrix.org",
"#space:envs.net",
"#space:unredacted.org",
"#science-space:matrix.org",
"#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org"
"#mathematics-on:matrix.org",
"#stickers-and-emojis:tastytea.de"
],
"rooms": [
"#cinny:matrix.org",
@@ -28,7 +22,7 @@
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
],
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
"servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
},
"hashRouter": {

7227
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "cinny",
"version": "4.10.2",
"version": "4.11.1",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
@@ -10,11 +10,53 @@
"scripts": {
"start": "vite",
"build": "vite build",
"lint": "yarn check:eslint && yarn check:prettier",
"preview": "vite preview",
"lint": "npm run check:eslint && npm run check:prettier",
"check:eslint": "eslint src/*",
"check:prettier": "prettier --check .",
"fix:prettier": "prettier --write .",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"prepare": "husky install",
"commit": "git-cz",
"semantic-release": "semantic-release"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": "eslint",
"*": "prettier --ignore-unknown --write"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"release": {
"branches": [
"dev"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec",
{
"prepareCmd": "node scripts/update-version.js ${nextRelease.version}"
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"package-lock.json",
"src/app/features/settings/about/About.tsx",
"src/app/pages/auth/AuthFooter.tsx",
"src/app/pages/client/WelcomePage.tsx"
],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
"@semantic-release/github"
]
},
"keywords": [],
"author": "Ajay Bura",
@@ -43,7 +85,7 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
"folds": "2.4.0",
"folds": "2.6.2",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@@ -52,9 +94,10 @@
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
"linkify-react": "4.1.3",
"linkifyjs": "4.1.3",
"linkify-react": "4.3.2",
"linkifyjs": "4.3.2",
"matrix-js-sdk": "38.2.0",
"matrix-widget-api": "1.13.0",
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
"prismjs": "1.30.0",
@@ -67,18 +110,21 @@
"react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-range": "1.8.14",
"react-router-dom": "6.20.0",
"react-router-dom": "6.30.3",
"sanitize-html": "2.12.1",
"slate": "0.112.0",
"slate-dom": "0.112.2",
"slate-history": "0.110.3",
"slate-react": "0.112.1",
"slate": "0.123.0",
"slate-dom": "0.123.0",
"slate-history": "0.113.1",
"slate-react": "0.123.0",
"ua-parser-js": "1.0.35"
},
"devDependencies": {
"@element-hq/element-call-embedded": "0.16.3",
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1",
"@semantic-release/exec": "7.1.0",
"@semantic-release/git": "10.0.1",
"@types/chroma-js": "3.1.1",
"@types/file-saver": "2.0.5",
"@types/is-hotkey": "0.1.10",
@@ -93,6 +139,7 @@
"@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3",
"cz-conventional-changelog": "3.3.0",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0",
@@ -100,7 +147,10 @@
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"husky": "9.1.7",
"lint-staged": "16.3.2",
"prettier": "2.8.1",
"semantic-release": "25.0.3",
"typescript": "4.9.4",
"vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",

48
scripts/update-version.js Normal file
View File

@@ -0,0 +1,48 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const version = process.argv[2];
if (!version) {
console.error("Version argument missing");
process.exit(1);
}
const root = path.resolve(__dirname, "..");
const newVersionTag = `v${version}`;
// Update package.json + package-lock.json safely
execSync(`npm version ${version} --no-git-tag-version`, {
cwd: root,
stdio: "inherit",
});
console.log(`Updated package.json and package-lock.json → ${version}`);
// Update UI version references
const files = [
"src/app/features/settings/about/About.tsx",
"src/app/pages/auth/AuthFooter.tsx",
"src/app/pages/client/WelcomePage.tsx",
];
files.forEach((filePath) => {
const absPath = path.join(root, filePath);
if (!fs.existsSync(absPath)) {
console.warn(`File not found: ${filePath}`);
return;
}
const content = fs.readFileSync(absPath, "utf8");
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
fs.writeFileSync(absPath, updated);
console.log(`Updated ${filePath}${newVersionTag}`);
});

View File

@@ -51,8 +51,12 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
},
location.pathname
);
if (spaceMatch?.params.spaceIdOrAlias) {
navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
const decodedSpaceIdOrAlias =
encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias);
if (decodedSpaceIdOrAlias) {
navigate(getSpacePath(decodedSpaceIdOrAlias));
return;
}
if (

View File

@@ -0,0 +1,66 @@
import React, { ReactNode, useCallback, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { config } from 'folds';
import {
CallEmbedContextProvider,
CallEmbedRefContextProvider,
useCallHangupEvent,
useCallJoined,
useCallThemeSync,
useCallMemberSoundSync,
} from '../hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
function CallUtils({ embed }: { embed: CallEmbed }) {
const setCallEmbed = useSetAtom(callEmbedAtom);
useCallMemberSoundSync(embed);
useCallThemeSync(embed);
useCallHangupEvent(
embed,
useCallback(() => {
setCallEmbed(undefined);
}, [setCallEmbed])
);
return null;
}
type CallEmbedProviderProps = {
children?: ReactNode;
};
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
const callEmbed = useAtomValue(callEmbedAtom);
const callEmbedRef = useRef<HTMLDivElement>(null);
const joined = useCallJoined(callEmbed);
const selectedRoom = useSelectedRoom();
const chat = useAtomValue(callChatAtom);
const screenSize = useScreenSizeContext();
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
const callVisible = callEmbed && selectedRoom === callEmbed.roomId && joined && !chatOnlyView;
return (
<CallEmbedContextProvider value={callEmbed}>
{callEmbed && <CallUtils embed={callEmbed} />}
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider>
<div
data-call-embed-container
style={{
visibility: callVisible ? undefined : 'hidden',
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '50%',
}}
ref={callEmbedRef}
/>
</CallEmbedContextProvider>
);
}

View File

@@ -0,0 +1,45 @@
import FocusTrap from 'focus-trap-react';
import { as, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import React, { ReactNode } from 'react';
import { ModalWide } from '../styles/Modal.css';
import { stopPropagation } from '../utils/keyboard';
export type RenderViewerProps = {
src: string;
alt: string;
requestClose: () => void;
};
type ImageOverlayProps = RenderViewerProps & {
viewer: boolean;
renderViewer: (props: RenderViewerProps) => ReactNode;
};
export const ImageOverlay = as<'div', ImageOverlayProps>(
({ src, alt, viewer, requestClose, renderViewer, ...props }, ref) => (
<Overlay {...props} ref={ref} open={viewer} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => requestClose(),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
className={ModalWide}
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
{renderViewer({
src,
alt,
requestClose,
})}
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)
);

View File

@@ -16,34 +16,24 @@ import {
import { JoinRule } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '../utils/keyboard';
import { getRoomIconSrc } from '../utils/room';
export type ExtraJoinRules = 'knock_restricted';
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
useMemo(
() => ({
[JoinRule.Invite]: Icons.HashLock,
[JoinRule.Knock]: Icons.HashLock,
knock_restricted: Icons.Hash,
[JoinRule.Restricted]: Icons.Hash,
[JoinRule.Public]: Icons.HashGlobe,
[JoinRule.Private]: Icons.HashLock,
[JoinRule.Invite]: getRoomIconSrc(Icons, roomType, JoinRule.Invite),
[JoinRule.Knock]: getRoomIconSrc(Icons, roomType, JoinRule.Knock),
knock_restricted: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
[JoinRule.Restricted]: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
}),
[]
);
export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
useMemo(
() => ({
[JoinRule.Invite]: Icons.SpaceLock,
[JoinRule.Knock]: Icons.SpaceLock,
knock_restricted: Icons.Space,
[JoinRule.Restricted]: Icons.Space,
[JoinRule.Public]: Icons.SpaceGlobe,
[JoinRule.Private]: Icons.SpaceLock,
}),
[]
[roomType]
);
type JoinRuleLabels = Record<ExtendedJoinRules, string>;

View File

@@ -2,43 +2,39 @@ import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
import { CreateRoomAccess } from './types';
export enum CreateRoomKind {
Private = 'private',
Restricted = 'restricted',
Public = 'public',
}
type CreateRoomKindSelectorProps = {
value?: CreateRoomKind;
onSelect: (value: CreateRoomKind) => void;
type CreateRoomAccessSelectorProps = {
value?: CreateRoomAccess;
onSelect: (value: CreateRoomAccess) => void;
canRestrict?: boolean;
disabled?: boolean;
getIcon: (kind: CreateRoomKind) => IconSrc;
getIcon: (access: CreateRoomAccess) => IconSrc;
};
export function CreateRoomKindSelector({
export function CreateRoomAccessSelector({
value,
onSelect,
canRestrict,
disabled,
getIcon,
}: CreateRoomKindSelectorProps) {
}: CreateRoomAccessSelectorProps) {
return (
<Box shrink="No" direction="Column" gap="100">
{canRestrict && (
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomKind.Restricted ? 'Primary' : 'SurfaceVariant'}
variant={value === CreateRoomAccess.Restricted ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomKind.Restricted}
onClick={() => onSelect(CreateRoomKind.Restricted)}
aria-pressed={value === CreateRoomAccess.Restricted}
onClick={() => onSelect(CreateRoomAccess.Restricted)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomKind.Restricted)} />}
after={value === CreateRoomKind.Restricted && <Icon src={Icons.Check} />}
before={<Icon size="400" src={getIcon(CreateRoomAccess.Restricted)} />}
after={value === CreateRoomAccess.Restricted && <Icon src={Icons.Check} />}
>
<Text size="H6">Restricted</Text>
<Text size="T300" priority="300">
@@ -49,18 +45,18 @@ export function CreateRoomKindSelector({
)}
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomKind.Private ? 'Primary' : 'SurfaceVariant'}
variant={value === CreateRoomAccess.Private ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomKind.Private}
onClick={() => onSelect(CreateRoomKind.Private)}
aria-pressed={value === CreateRoomAccess.Private}
onClick={() => onSelect(CreateRoomAccess.Private)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomKind.Private)} />}
after={value === CreateRoomKind.Private && <Icon src={Icons.Check} />}
before={<Icon size="400" src={getIcon(CreateRoomAccess.Private)} />}
after={value === CreateRoomAccess.Private && <Icon src={Icons.Check} />}
>
<Text size="H6">Private</Text>
<Text size="T300" priority="300">
@@ -70,18 +66,18 @@ export function CreateRoomKindSelector({
</SequenceCard>
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomKind.Public ? 'Primary' : 'SurfaceVariant'}
variant={value === CreateRoomAccess.Public ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomKind.Public}
onClick={() => onSelect(CreateRoomKind.Public)}
aria-pressed={value === CreateRoomAccess.Public}
onClick={() => onSelect(CreateRoomAccess.Public)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomKind.Public)} />}
after={value === CreateRoomKind.Public && <Icon src={Icons.Check} />}
before={<Icon size="400" src={getIcon(CreateRoomAccess.Public)} />}
after={value === CreateRoomAccess.Public && <Icon src={Icons.Check} />}
>
<Text size="H6">Public</Text>
<Text size="T300" priority="300">

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
import { CreateRoomType } from './types';
import { BetaNoticeBadge } from '../BetaNoticeBadge';
type CreateRoomTypeSelectorProps = {
value?: CreateRoomType;
onSelect: (value: CreateRoomType) => void;
disabled?: boolean;
getIcon: (type: CreateRoomType) => IconSrc;
};
export function CreateRoomTypeSelector({
value,
onSelect,
disabled,
getIcon,
}: CreateRoomTypeSelectorProps) {
return (
<Box shrink="No" direction="Column" gap="100">
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomType.TextRoom ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomType.TextRoom}
onClick={() => onSelect(CreateRoomType.TextRoom)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomType.TextRoom)} />}
after={value === CreateRoomType.TextRoom && <Icon src={Icons.Check} />}
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Chat Room
</Text>
<Text size="T300" priority="300" truncate>
- Messages, photos, and videos.
</Text>
</Box>
</SettingTile>
</SequenceCard>
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomType.VoiceRoom ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomType.VoiceRoom}
onClick={() => onSelect(CreateRoomType.VoiceRoom)}
disabled={disabled}
>
<SettingTile
before={<Icon size="400" src={getIcon(CreateRoomType.VoiceRoom)} />}
after={value === CreateRoomType.VoiceRoom && <Icon src={Icons.Check} />}
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Voice Room
</Text>
<Text size="T300" priority="300" truncate>
- Live audio and video conversations.
</Text>
<BetaNoticeBadge />
</Box>
</SettingTile>
</SequenceCard>
</Box>
);
}

View File

@@ -1,5 +1,6 @@
export * from './CreateRoomKindSelector';
export * from './CreateRoomAccessSelector';
export * from './CreateRoomAliasInput';
export * from './RoomVersionSelector';
export * from './utils';
export * from './AdditionalCreatorInput';
export * from './types';

View File

@@ -0,0 +1,10 @@
export enum CreateRoomType {
TextRoom = 'text',
VoiceRoom = 'voice',
}
export enum CreateRoomAccess {
Private = 'private',
Restricted = 'restricted',
Public = 'public',
}

View File

@@ -7,10 +7,10 @@ import {
Room,
} from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { CreateRoomKind } from './CreateRoomKindSelector';
import { RoomType, StateEvent } from '../../../types/matrix/room';
import { getViaServers } from '../../plugins/via-servers';
import { getMxIdServer } from '../../utils/matrix';
import { CreateRoomAccess } from './types';
export const createRoomCreationContent = (
type: RoomType | undefined,
@@ -32,7 +32,7 @@ export const createRoomCreationContent = (
};
export const createRoomJoinRulesState = (
kind: CreateRoomKind,
access: CreateRoomAccess,
parent: Room | undefined,
knock: boolean
) => {
@@ -40,13 +40,13 @@ export const createRoomJoinRulesState = (
join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
};
if (kind === CreateRoomKind.Public) {
if (access === CreateRoomAccess.Public) {
content = {
join_rule: JoinRule.Public,
};
}
if (kind === CreateRoomKind.Restricted && parent) {
if (access === CreateRoomAccess.Restricted && parent) {
content = {
join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
allow: [
@@ -74,6 +74,10 @@ export const createRoomParentState = (parent: Room) => ({
},
});
const createSpacePowerLevelsOverride = () => ({
events_default: 50,
});
export const createRoomEncryptionState = () => ({
type: 'm.room.encryption',
state_key: '',
@@ -82,11 +86,23 @@ export const createRoomEncryptionState = () => ({
},
});
export const createRoomCallState = () => ({
type: 'org.matrix.msc3401.call',
state_key: '',
content: {},
});
export const createVoiceRoomPowerLevelsOverride = () => ({
events: {
[StateEvent.GroupCallMemberPrefix]: 0,
},
});
export type CreateRoomData = {
version: string;
type?: RoomType;
parent?: Room;
kind: CreateRoomKind;
access: CreateRoomAccess;
name: string;
topic?: string;
aliasLocalPart?: string;
@@ -106,7 +122,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
initialState.push(createRoomParentState(data.parent));
}
initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
if (data.type === RoomType.Call) {
initialState.push(createRoomCallState());
}
initialState.push(createRoomJoinRulesState(data.access, data.parent, data.knock));
const options: ICreateRoomOpts = {
room_version: data.version,
@@ -118,9 +138,15 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
data.allowFederation,
data.additionalCreators
),
power_level_content_override:
data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined,
initial_state: initialState,
};
if (data.type === RoomType.Space) {
options.power_level_content_override = createSpacePowerLevelsOverride();
}
const result = await mx.createRoom(options);
if (data.parent) {

View File

@@ -88,6 +88,8 @@ export function EmoticonAutocomplete({
{autoCompleteEmoticon.map((emoticon) => {
const isCustomEmoji = 'url' in emoticon;
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
return (
<MenuItem
key={emoticon.shortcode + key}
@@ -98,11 +100,11 @@ export function EmoticonAutocomplete({
}
onClick={() => handleAutocomplete(key, emoticon.shortcode)}
before={
isCustomEmoji ? (
isCustomEmoji && customEmojiUrl ? (
<Box
shrink="No"
as="img"
src={mxcUrlToHttp(mx, key, useAuthentication) || key}
src={customEmojiUrl}
alt={emoticon.shortcode}
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
/>

View File

@@ -169,12 +169,13 @@ export function RoomMentionAutocomplete({
<RoomIcon
size="50"
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
roomType={room.getType()}
filled
/>
)}
/>
) : (
<RoomIcon size="100" joinRule={room.getJoinRule()} space={room.isSpaceRoom()} />
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
)}
</Avatar>
}

View File

@@ -228,9 +228,13 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
children: [{ text }],
}));
const childCode = node.children[0];
const className =
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
const prefix = { text: `${mdSequence}${className.replace('language-', '')}` };
const attribs =
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs : undefined;
const languageClass = attribs?.class;
const customLabel = attribs?.['data-label'];
const prefix = {
text: `${mdSequence}${customLabel ?? languageClass?.replace('language-', '') ?? ''}`,
};
const suffix = { text: mdSequence };
return [
{ type: BlockType.Paragraph, children: [prefix] },

View File

@@ -212,9 +212,10 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.type === BlockType.CodeBlock) return;
if (node.type === BlockType.Mention) {
if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
if (node.name === '@room') {
mentionData.room = true;
}
if (isUserId(node.id) && node.id !== mx.getUserId()) {
mentionData.users.add(node.id);
}

View File

@@ -202,8 +202,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
pack.meta.avatar;
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
return (
<ImageGroupIcon
@@ -266,7 +265,7 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar;
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
return (
<ImageGroupIcon

View File

@@ -68,7 +68,7 @@ export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiIte
loading="lazy"
className={css.CustomEmojiImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
/>
</Box>
);
@@ -98,7 +98,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
loading="lazy"
className={css.StickerImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
/>
</Box>
);

View File

@@ -27,7 +27,8 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
const [downloadState, download] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);

View File

@@ -389,6 +389,8 @@ export function MLocation({ content }: MLocationProps) {
const geoUri = content.geo_uri;
if (typeof geoUri !== 'string') return <BrokenContent />;
const location = parseGeoUri(geoUri);
if (!location) return <BrokenContent />;
return (
<Box direction="Column" alignItems="Start" gap="100">
<Text size="T400">{geoUri}</Text>

View File

@@ -54,7 +54,8 @@ export function AudioContent({
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);

View File

@@ -86,7 +86,8 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
const [textState, loadText] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
@@ -176,7 +177,8 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);
@@ -253,7 +255,8 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
const [downloadState, download] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
: await downloadMedia(mediaUrl);

View File

@@ -87,7 +87,8 @@ export const ImageContent = as<'div', ImageContentProps>(
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)

View File

@@ -23,7 +23,8 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
throw new Error('Failed to load thumbnail');
}
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)

View File

@@ -81,7 +81,8 @@ export const VideoContent = as<'div', VideoContentProps>(
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
if (!mediaUrl) throw new Error('Invalid media URL');
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
decryptFile(encBuf, mimeType, encInfo)

View File

@@ -2,7 +2,7 @@ import { JoinRule } from 'matrix-js-sdk';
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
import * as css from './RoomAvatar.css';
import { joinRuleToIconSrc } from '../../utils/room';
import { getRoomIconSrc } from '../../utils/room';
import colorMXID from '../../../util/colorMXID';
type RoomAvatarProps = {
@@ -44,13 +44,9 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
export const RoomIcon = forwardRef<
SVGSVGElement,
Omit<ComponentProps<typeof Icon>, 'src'> & {
joinRule: JoinRule;
space?: boolean;
joinRule?: JoinRule;
roomType?: string;
}
>(({ joinRule, space, ...props }, ref) => (
<Icon
src={joinRuleToIconSrc(Icons, joinRule, space || false) ?? Icons.Hash}
{...props}
ref={ref}
/>
>(({ joinRule, roomType, ...props }, ref) => (
<Icon src={getRoomIconSrc(Icons, roomType, joinRule)} {...props} ref={ref} />
));

View File

@@ -17,6 +17,7 @@ export const SequenceCard = as<
firstChild,
lastChild,
outlined,
mergeBorder,
...props
},
ref
@@ -24,7 +25,7 @@ export const SequenceCard = as<
<Box
as={AsSequenceCard}
className={classNames(
css.SequenceCard({ radii, outlined }),
css.SequenceCard({ radii, outlined, mergeBorder }),
ContainerColor({ variant }),
className
)}

View File

@@ -11,7 +11,7 @@ export const SequenceCard = recipe({
},
borderStyle: 'solid',
borderWidth: outlinedWidth,
borderBottomWidth: 0,
selectors: {
'&:first-child, :not(&) + &': {
borderTopLeftRadius: [radii],
@@ -20,7 +20,6 @@ export const SequenceCard = recipe({
'&:last-child, &:not(:has(+&))': {
borderBottomLeftRadius: [radii],
borderBottomRightRadius: [radii],
borderBottomWidth: outlinedWidth,
},
[`&[data-first-child="true"]`]: {
borderTopLeftRadius: [radii],
@@ -74,6 +73,16 @@ export const SequenceCard = recipe({
},
},
},
mergeBorder: {
true: {
borderBottomWidth: 0,
selectors: {
'&:last-child, &:not(:has(+&))': {
borderBottomWidth: outlinedWidth,
},
},
},
},
},
defaultVariants: {
radii: '400',

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { as, Avatar } from 'folds';
import classNames from 'classnames';
import * as css from './styles.css';
type StackedAvatarProps = {
radii?: '0' | '300' | '400' | '500' | 'Pill' | 'Inherit' | undefined;
};
export const StackedAvatar = as<'span', css.StackedAvatarVariants & StackedAvatarProps>(
({ size, variant, className, ...props }, ref) => (
<Avatar
size={size}
className={classNames(css.StackedAvatar({ size, variant }), className)}
{...props}
ref={ref}
/>
)
);

View File

@@ -0,0 +1 @@
export * from './StackedAvatar';

View File

@@ -0,0 +1,59 @@
import { ComplexStyleRule } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { color, config, ContainerColor, toRem } from 'folds';
const getVariant = (variant: ContainerColor): ComplexStyleRule => ({
outlineColor: color[variant].Container,
});
export const StackedAvatar = recipe({
base: {
backgroundColor: color.Surface.Container,
outlineStyle: 'solid',
selectors: {
'&:first-child': {
marginLeft: 0,
},
'button&': {
cursor: 'pointer',
},
},
},
variants: {
size: {
'200': {
marginLeft: toRem(-6),
outlineWidth: config.borderWidth.B300,
},
'300': {
marginLeft: toRem(-9),
outlineWidth: config.borderWidth.B400,
},
'400': {
marginLeft: toRem(-10.5),
outlineWidth: config.borderWidth.B500,
},
'500': {
marginLeft: toRem(-13),
outlineWidth: config.borderWidth.B600,
},
},
variant: {
Background: getVariant('Background'),
Surface: getVariant('Surface'),
SurfaceVariant: getVariant('SurfaceVariant'),
Primary: getVariant('Primary'),
Secondary: getVariant('Secondary'),
Success: getVariant('Success'),
Warning: getVariant('Warning'),
Critical: getVariant('Critical'),
},
},
defaultVariants: {
size: '400',
variant: 'Surface',
},
});
export type StackedAvatarVariants = RecipeVariants<typeof StackedAvatar>;

View File

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

View File

@@ -23,6 +23,11 @@ export const UrlPreviewImg = style([
objectPosition: 'center',
flexShrink: 0,
overflow: 'hidden',
cursor: 'pointer',
':hover': {
filter: 'brightness(0.8)',
},
},
]);

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
import { ImageOverlay } from '../ImageOverlay';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -12,6 +13,8 @@ import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { ImageViewer } from '../image-viewer';
import { onEnterOrSpace } from '../../utils/keyboard';
const linkStyles = { color: color.Success.Main };
@@ -19,6 +22,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState(false);
const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
);
@@ -30,11 +34,41 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
const thumbUrl = mxcUrlToHttp(
mx,
prev['og:image'] || '',
useAuthentication,
256,
256,
'scale',
false
);
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
return (
<>
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />}
{thumbUrl && (
<UrlPreviewImg
src={thumbUrl}
alt={prev['og:title']}
title={prev['og:title']}
tabIndex={0}
onKeyDown={(evt) => onEnterOrSpace(() => setViewer(true))(evt)}
onClick={() => setViewer(true)}
/>
)}
{imgUrl && (
<ImageOverlay
src={imgUrl}
alt={prev['og:title']}
viewer={viewer}
requestClose={() => {
setViewer(false);
}}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
<UrlPreviewContent>
<Text
style={linkStyles}
@@ -42,7 +76,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
as="a"
href={url}
target="_blank"
rel="no-referrer"
rel="noreferrer"
size="T200"
priority="300"
>

View File

@@ -323,7 +323,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
)}
/>
) : (
<RoomIcon size="100" joinRule={room.getJoinRule()} />
<RoomIcon size="100" joinRule={room.getJoinRule()} roomType={room.getType()} />
)}
</Avatar>
}

View File

@@ -20,6 +20,16 @@ export type AutoDiscoveryInfo = Record<string, unknown> & {
'm.identity_server'?: {
base_url: string;
};
'org.matrix.msc2965.authentication'?: {
account?: string;
issuer?: string;
};
'org.matrix.msc4143.rtc_foci'?: [
{
livekit_service_url: string;
type: 'livekit';
}
];
};
export const autoDiscovery = async (

View File

@@ -291,7 +291,11 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
)}
/>
) : (
<RoomIcon size="200" joinRule={room.getJoinRule()} />
<RoomIcon
size="200"
joinRule={room.getJoinRule()}
roomType={room.getType()}
/>
)}
</Avatar>
}

View File

@@ -0,0 +1,226 @@
import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds';
import React, { useCallback } from 'react';
import { useSetAtom } from 'jotai';
import { StatusDivider } from './components';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { callEmbedAtom } from '../../state/callEmbed';
type MicrophoneButtonProps = {
enabled: boolean;
onToggle: () => Promise<unknown>;
disabled?: boolean;
};
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="300"
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon size="100" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
type SoundButtonProps = {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
};
function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="300"
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon
size="100"
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
filled={!enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type VideoButtonProps = {
enabled: boolean;
onToggle: () => Promise<unknown>;
disabled?: boolean;
};
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="300"
size="300"
onClick={() => onToggle()}
outlined
disabled={disabled}
>
<Icon
size="100"
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
filled={enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type ScreenShareButtonProps = {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
};
function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonProps) {
return (
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="300"
size="300"
onClick={onToggle}
outlined
disabled={disabled}
>
<Icon size="100" src={Icons.ScreenShare} filled={enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
export function CallControl({
callEmbed,
compact,
callJoined,
}: {
callEmbed: CallEmbed;
compact: boolean;
callJoined: boolean;
}) {
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
const setCallEmbed = useSetAtom(callEmbedAtom);
const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed])
);
const exiting =
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
const handleHangup = () => {
if (!callJoined) {
setCallEmbed(undefined);
return;
}
hangup();
};
return (
<Box shrink="No" alignItems="Center" gap="300">
<Box alignItems="Inherit" gap="200">
<MicrophoneButton
enabled={microphone}
onToggle={() => callEmbed.control.toggleMicrophone()}
disabled={!callJoined}
/>
<SoundButton
enabled={sound}
onToggle={() => callEmbed.control.toggleSound()}
disabled={!callJoined}
/>
{!compact && <StatusDivider />}
<VideoButton
enabled={video}
onToggle={() => callEmbed.control.toggleVideo()}
disabled={!callJoined}
/>
{!compact && (
<ScreenShareButton
enabled={screenshare}
onToggle={() => callEmbed.control.toggleScreenshare()}
disabled={!callJoined}
/>
)}
</Box>
<StatusDivider />
<Chip
variant="Critical"
radii="Pill"
fill="Soft"
before={
exiting ? (
<Spinner variant="Critical" fill="Soft" size="50" />
) : (
<Icon size="50" src={Icons.PhoneDown} filled />
)
}
disabled={exiting}
outlined
onClick={handleHangup}
>
{!compact && (
<Text as="span" size="L400">
End
</Text>
)}
</Chip>
</Box>
);
}

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Room } from 'matrix-js-sdk';
import { Chip, Text } from 'folds';
import { useAtomValue } from 'jotai';
import { useRoomName } from '../../hooks/useRoomMeta';
import { RoomIcon } from '../../components/room-avatar';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { getAllParents, guessPerfectParent } from '../../utils/room';
import { useOrphanSpaces } from '../../state/hooks/roomList';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList';
import { mDirectAtom } from '../../state/mDirectList';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
type CallRoomNameProps = {
room: Room;
};
export function CallRoomName({ room }: CallRoomNameProps) {
const mx = useMatrixClient();
const name = useRoomName(room);
const roomToParents = useAtomValue(roomToParentsAtom);
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
const mDirects = useAtomValue(mDirectAtom);
const dm = mDirects.has(room.roomId);
const allRoomsSet = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allRoomsSet);
const allParents = getAllParents(roomToParents, room.roomId);
const orphanParents = allParents && orphanSpaces.filter((o) => allParents.has(o));
const perfectOrphanParent = orphanParents && guessPerfectParent(mx, room.roomId, orphanParents);
const { navigateRoom } = useRoomNavigate();
return (
<Chip
variant="Background"
radii="Pill"
before={
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
}
onClick={() => navigateRoom(room.roomId)}
>
<Text size="L400" truncate>
{name}
{!dm && perfectOrphanParent && (
<Text as="span" size="T200" priority="300">
{' •'} <b>{getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent}</b>
</Text>
)}
</Text>
</Chip>
);
}

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Box, Spinner } from 'folds';
import classNames from 'classnames';
import { LiveChip } from './LiveChip';
import * as css from './styles.css';
import { CallRoomName } from './CallRoomName';
import { CallControl } from './CallControl';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
import { MemberGlance } from './MemberGlance';
import { StatusDivider } from './components';
import { CallEmbed } from '../../plugins/call/CallEmbed';
import { useCallJoined } from '../../hooks/useCallEmbed';
import { useCallSpeakers } from '../../hooks/useCallSpeakers';
import { MemberSpeaking } from './MemberSpeaking';
type CallStatusProps = {
callEmbed: CallEmbed;
};
export function CallStatus({ callEmbed }: CallStatusProps) {
const { room } = callEmbed;
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const screenSize = useScreenSize();
const callJoined = useCallJoined(callEmbed);
const speakers = useCallSpeakers(callEmbed);
const compact = screenSize === ScreenSize.Mobile;
const memberVisible = callJoined && callMembers.length > 0;
return (
<Box
className={classNames(css.CallStatus, ContainerColor({ variant: 'Background' }))}
shrink="No"
gap="400"
alignItems={compact ? undefined : 'Center'}
direction={compact ? 'Column' : 'Row'}
>
<Box grow="Yes" alignItems="Center" gap="200">
{memberVisible ? (
<Box shrink="No">
<LiveChip count={callMembers.length} room={room} members={callMembers} />
</Box>
) : (
<Spinner variant="Secondary" size="200" />
)}
<Box grow="Yes" alignItems="Center" gap="Inherit">
{!compact && (
<>
<CallRoomName room={room} />
{speakers.size > 0 && (
<>
<StatusDivider />
<span data-spacing-node />
<MemberSpeaking room={room} speakers={speakers} />
</>
)}
</>
)}
</Box>
{memberVisible && (
<Box shrink="No">
<MemberGlance room={room} members={callMembers} speakers={speakers} />
</Box>
)}
</Box>
{memberVisible && !compact && <StatusDivider />}
<Box shrink="No" alignItems="Center" gap="Inherit">
{compact && (
<Box grow="Yes">
<CallRoomName room={room} />
</Box>
)}
<CallControl callJoined={callJoined} compact={compact} callEmbed={callEmbed} />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,137 @@
import React, { MouseEventHandler, useState } from 'react';
import {
Avatar,
Badge,
Box,
Chip,
config,
Icon,
Icons,
Menu,
MenuItem,
PopOut,
RectCords,
Scroll,
Text,
toRem,
} from 'folds';
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import FocusTrap from 'focus-trap-react';
import { Room } from 'matrix-js-sdk';
import * as css from './styles.css';
import { stopPropagation } from '../../utils/keyboard';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { UserAvatar } from '../../components/user-avatar';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { getMouseEventCords } from '../../utils/dom';
type LiveChipProps = {
room: Room;
members: CallMembership[];
count: number;
};
export function LiveChip({ count, room, members }: LiveChipProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const openUserProfile = useOpenUserRoomProfile();
const [cords, setCords] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
return (
<PopOut
anchor={cords}
position="Top"
align="Start"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu
style={{
maxHeight: '75vh',
maxWidth: toRem(300),
display: 'flex',
}}
>
<Box grow="Yes">
<Scroll size="0" hideTrack visibility="Hover">
<Box direction="Column" style={{ padding: config.space.S100 }}>
{members.map((callMember) => {
const userId = callMember.sender;
if (!userId) return null;
const name =
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
: undefined;
return (
<MenuItem
key={callMember.membershipID}
size="400"
variant="Surface"
radii="300"
style={{ paddingLeft: config.space.S200 }}
onClick={(evt) =>
openUserProfile(
room.roomId,
undefined,
userId,
getMouseEventCords(evt.nativeEvent),
'Right'
)
}
before={
<Avatar size="200" radii="400">
<UserAvatar
userId={userId}
src={avatarUrl}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
}
>
<Text size="T300" truncate>
{name}
</Text>
</MenuItem>
);
})}
</Box>
</Scroll>
</Box>
</Menu>
</FocusTrap>
}
>
<Chip
variant="Surface"
fill="Soft"
before={<Badge variant="Critical" fill="Solid" size="200" />}
after={<Icon size="50" src={cords ? Icons.ChevronBottom : Icons.ChevronTop} />}
radii="Pill"
onClick={handleOpenMenu}
>
<Text className={css.LiveChipText} as="span" size="L400" truncate>
{count} Live
</Text>
</Chip>
</PopOut>
);
}

View File

@@ -0,0 +1,75 @@
import { Box, config, Icon, Icons, Text } from 'folds';
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import React from 'react';
import { Room } from 'matrix-js-sdk';
import { UserAvatar } from '../../components/user-avatar';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { StackedAvatar } from '../../components/stacked-avatar';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { getMouseEventCords } from '../../utils/dom';
import * as css from './styles.css';
type MemberGlanceProps = {
room: Room;
members: CallMembership[];
speakers: Set<string>;
max?: number;
};
export function MemberGlance({ room, members, speakers, max = 6 }: MemberGlanceProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const openUserProfile = useOpenUserRoomProfile();
const visibleMembers = members.slice(0, max);
const remainingCount = max && members.length > max ? members.length - max : 0;
return (
<Box alignItems="Center">
{visibleMembers.map((callMember) => {
const userId = callMember.sender;
if (!userId) return null;
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
: undefined;
return (
<StackedAvatar
key={callMember.membershipID}
className={speakers.has(callMember.sender) ? css.SpeakerAvatarOutline : undefined}
title={name}
as="button"
variant="Background"
size="200"
radii="Pill"
onClick={(evt) =>
openUserProfile(
room.roomId,
undefined,
userId,
getMouseEventCords(evt.nativeEvent),
'Top'
)
}
>
<UserAvatar
userId={userId}
src={avatarUrl}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</StackedAvatar>
);
})}
{remainingCount > 0 && (
<Text size="L400" style={{ paddingLeft: config.space.S100 }}>
+{remainingCount}
</Text>
)}
</Box>
);
}

View File

@@ -0,0 +1,78 @@
import { Room } from 'matrix-js-sdk';
import React from 'react';
import { Box, Icon, Icons, Text } from 'folds';
import { getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
type MemberSpeakingProps = {
room: Room;
speakers: Set<string>;
};
export function MemberSpeaking({ room, speakers }: MemberSpeakingProps) {
const speakingNames = Array.from(speakers).map(
(userId) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId
);
return (
<Box alignItems="Center" gap="100">
<Icon size="100" src={Icons.Mic} filled />
<Text size="T200" truncate>
{speakingNames.length === 1 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' is speaking...'}
</Text>
</>
)}
{speakingNames.length === 2 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
</Text>
<b>{speakingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' are speaking...'}
</Text>
</>
)}
{speakingNames.length === 3 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{', '}
</Text>
<b>{speakingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
</Text>
<b>{speakingNames[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' are speaking...'}
</Text>
</>
)}
{speakingNames.length > 3 && (
<>
<b>{speakingNames[0]}</b>
<Text as="span" size="Inherit" priority="300">
{', '}
</Text>
<b>{speakingNames[1]}</b>
<Text as="span" size="Inherit" priority="300">
{', '}
</Text>
<b>{speakingNames[2]}</b>
<Text as="span" size="Inherit" priority="300">
{' and '}
</Text>
<b>{speakingNames.length - 3} others</b>
<Text as="span" size="Inherit" priority="300">
{' are speaking...'}
</Text>
</>
)}
</Text>
</Box>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { Line } from 'folds';
import * as css from './styles.css';
export function StatusDivider() {
return (
<Line variant="Background" size="300" direction="Vertical" className={css.ControlDivider} />
);
}

View File

@@ -0,0 +1 @@
export * from './CallStatus';

View File

@@ -0,0 +1,21 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
export const LiveChipText = style({
color: color.Critical.Main,
});
export const CallStatus = style([
{
padding: `${toRem(6)} ${config.space.S200}`,
borderTop: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
},
]);
export const ControlDivider = style({
height: toRem(16),
});
export const SpeakerAvatarOutline = style({
boxShadow: `0 0 0 ${config.borderWidth.B600} ${color.Success.Main}`,
});

View File

@@ -0,0 +1,203 @@
import React, { MouseEventHandler, useCallback, useRef, useState } from 'react';
import {
Box,
Button,
config,
Icon,
IconButton,
Icons,
Menu,
MenuItem,
PopOut,
RectCords,
Spinner,
Text,
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
import {
ChatButton,
ControlDivider,
MicrophoneButton,
ScreenShareButton,
SoundButton,
VideoButton,
} from './Controls';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { useResizeObserver } from '../../hooks/useResizeObserver';
import { stopPropagation } from '../../utils/keyboard';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
type CallControlsProps = {
callEmbed: CallEmbed;
};
export function CallControls({ callEmbed }: CallControlsProps) {
const controlRef = useRef<HTMLDivElement>(null);
const [compact, setCompact] = useState(document.body.clientWidth < 500);
useResizeObserver(
useCallback(() => {
const element = controlRef.current;
if (!element) return;
setCompact(element.clientWidth < 500);
}, []),
useCallback(() => controlRef.current, [])
);
const { microphone, video, sound, screenshare, spotlight } = useCallControlState(
callEmbed.control
);
const [cords, setCords] = useState<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
const handleSpotlightClick = () => {
callEmbed.control.toggleSpotlight();
setCords(undefined);
};
const handleReactionsClick = () => {
callEmbed.control.toggleReactions();
setCords(undefined);
};
const handleSettingsClick = () => {
callEmbed.control.toggleSettings();
setCords(undefined);
};
const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed])
);
const exiting =
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
return (
<Box
ref={controlRef}
className={css.CallControlContainer}
justifyContent="Center"
alignItems="Center"
>
<SequenceCard
className={css.ControlCard}
variant="SurfaceVariant"
gap="400"
radii="500"
alignItems="Center"
justifyContent="SpaceBetween"
>
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
<MicrophoneButton
enabled={microphone}
onToggle={() => callEmbed.control.toggleMicrophone()}
/>
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
</Box>
{!compact && <ControlDivider />}
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
<ScreenShareButton
enabled={screenshare}
onToggle={() => callEmbed.control.toggleScreenshare()}
/>
</Box>
</Box>
{!compact && <ControlDivider />}
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
<ChatButton />
<PopOut
anchor={cords}
position="Top"
align="Center"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu>
<Box direction="Column" style={{ padding: config.space.S100 }}>
<MenuItem
size="300"
variant="Surface"
radii="300"
onClick={handleSpotlightClick}
>
<Text size="B300" truncate>
{spotlight ? 'Grid View' : 'Spotlight View'}
</Text>
</MenuItem>
<MenuItem
size="300"
variant="Surface"
radii="300"
onClick={handleReactionsClick}
>
<Text size="B300" truncate>
Reactions
</Text>
</MenuItem>
<MenuItem
size="300"
variant="Surface"
radii="300"
onClick={handleSettingsClick}
>
<Text size="B300" truncate>
Settings
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
>
<IconButton
variant="Surface"
fill="Soft"
radii="400"
size="400"
onClick={handleOpenMenu}
outlined
aria-pressed={!!cords}
>
<Icon size="400" src={Icons.VerticalDots} />
</IconButton>
</PopOut>
</Box>
<Box shrink="No" direction="Column">
<Button
style={{ minWidth: toRem(88) }}
variant="Critical"
fill="Solid"
onClick={hangup}
before={
exiting ? (
<Spinner variant="Critical" fill="Solid" size="200" />
) : (
<Icon src={Icons.PhoneDown} size="200" filled />
)
}
disabled={exiting}
>
<Text size="B400">End</Text>
</Button>
</Box>
</Box>
</SequenceCard>
</Box>
);
}

View File

@@ -0,0 +1,121 @@
import { CallMembership, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import React, { useState } from 'react';
import { Avatar, Box, Icon, Icons, Text } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { SequenceCard } from '../../components/sequence-card';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { useRoom } from '../../hooks/useRoom';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { UserAvatar } from '../../components/user-avatar';
import { getMouseEventCords } from '../../utils/dom';
import * as css from './styles.css';
interface MemberWithMembershipData {
membershipData?: SessionMembershipData & {
'm.call.intent': 'video' | 'audio';
};
}
type CallMemberCardProps = {
member: CallMembership;
};
export function CallMemberCard({ member }: CallMemberCardProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
const openUserProfile = useOpenUserRoomProfile();
const userId = member.sender;
if (!userId) return null;
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = getMemberAvatarMxc(room, userId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined
: undefined;
const audioOnly =
(member as unknown as MemberWithMembershipData).membershipData?.['m.call.intent'] === 'audio';
return (
<SequenceCard
as="button"
key={member.membershipID}
className={css.CallMemberCard}
variant="SurfaceVariant"
radii="500"
onClick={(evt: any) =>
openUserProfile(
room.roomId,
undefined,
userId,
getMouseEventCords(evt.nativeEvent),
'Right'
)
}
>
<Box grow="Yes" gap="300" alignItems="Center">
<Avatar size="200" radii="400">
<UserAvatar
userId={userId}
src={avatarUrl}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
<Box grow="Yes">
<Text size="L400" truncate>
{name}
</Text>
</Box>
{audioOnly && <Icon src={Icons.VideoCameraMute} size="100" />}
</Box>
</SequenceCard>
);
}
export function CallMemberRenderer({
members,
max = 4,
}: {
members: CallMembership[];
max?: number;
}) {
const [viewMore, setViewMore] = useState(false);
const truncatedMembers = viewMore ? members : members.slice(0, 4);
const remaining = members.length - truncatedMembers.length;
return (
<>
{truncatedMembers.map((member) => (
<CallMemberCard key={member.membershipID} member={member} />
))}
{members.length > max && (
<SequenceCard
as="button"
className={css.CallMemberCard}
variant="SurfaceVariant"
radii="500"
onClick={() => setViewMore(!viewMore)}
>
<Box grow="Yes" gap="300" alignItems="Center">
{viewMore ? (
<Text size="L400" truncate>
Collapse
</Text>
) : (
<Text size="L400" truncate>
{remaining === 0 ? `+${remaining} Other` : `+${remaining} Others`}
</Text>
)}
</Box>
<Icon src={viewMore ? Icons.ChevronTop : Icons.ChevronBottom} size="100" />
</SequenceCard>
)}
</>
);
}

View File

@@ -0,0 +1,150 @@
import React, { RefObject, useRef } from 'react';
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { PrescreenControls } from './PrescreenControls';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { StateEvent } from '../../../types/matrix/room';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { CallMemberRenderer } from './CallMemberCard';
import * as css from './styles.css';
import { CallControls } from './CallControls';
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
function LivekitServerMissingMessage() {
return (
<Text style={{ margin: 'auto', color: color.Critical.Main }} size="L400" align="Center">
Your homeserver does not support calling. But you can still join call started by others.
</Text>
);
}
function JoinMessage({
hasParticipant,
livekitSupported,
}: {
hasParticipant?: boolean;
livekitSupported?: boolean;
}) {
if (hasParticipant) return null;
if (livekitSupported === false) {
return <LivekitServerMissingMessage />;
}
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
Voice chats empty Be the first to hop in!
</Text>
);
}
function NoPermissionMessage() {
return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
You don&#39;t have permission to join!
</Text>
);
}
function AlreadyInCallMessage() {
return (
<Text style={{ margin: 'auto', color: color.Warning.Main }} size="L400" align="Center">
Already in another call End the current call to join!
</Text>
);
}
function CallPrescreen() {
const mx = useMatrixClient();
const room = useRoom();
const livekitSupported = useLivekitSupport();
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const hasPermission = permissions.event(StateEvent.GroupCallMemberPrefix, mx.getSafeUserId());
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const hasParticipant = callMembers.length > 0;
const callEmbed = useCallEmbed();
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
const canJoin = hasPermission && (livekitSupported || hasParticipant);
return (
<Scroll variant="Surface" hideTrack>
<Box className={css.CallViewContent} alignItems="Center" justifyContent="Center">
<Box style={{ maxWidth: toRem(382), width: '100%' }} direction="Column" gap="100">
{hasParticipant && (
<Header size="300">
<Box grow="Yes" alignItems="Center">
<Text size="L400">Participant</Text>
</Box>
<Badge variant="Critical" fill="Solid" size="400">
<Text as="span" size="L400" truncate>
{callMembers.length} Live
</Text>
</Badge>
</Header>
)}
<CallMemberRenderer members={callMembers} />
<PrescreenControls canJoin={canJoin} />
<Box className={css.PrescreenMessage} alignItems="Center">
{!inOtherCall &&
(hasPermission ? (
<JoinMessage hasParticipant={hasParticipant} livekitSupported={livekitSupported} />
) : (
<NoPermissionMessage />
))}
{inOtherCall && <AlreadyInCallMessage />}
</Box>
</Box>
</Box>
</Scroll>
);
}
type CallJoinedProps = {
containerRef: RefObject<HTMLDivElement>;
joined: boolean;
};
function CallJoined({ joined, containerRef }: CallJoinedProps) {
const callEmbed = useCallEmbed();
return (
<Box grow="Yes" direction="Column">
<Box grow="Yes" ref={containerRef} />
{callEmbed && joined && <CallControls callEmbed={callEmbed} />}
</Box>
);
}
export function CallView() {
const room = useRoom();
const callContainerRef = useRef<HTMLDivElement>(null);
useCallEmbedPlacementSync(callContainerRef);
const callEmbed = useCallEmbed();
const callJoined = useCallJoined(callEmbed);
const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
return (
<Box
className={ContainerColor({ variant: 'Surface' })}
style={{ minWidth: toRem(280) }}
grow="Yes"
>
{!currentJoined && <CallPrescreen />}
<CallJoined joined={currentJoined} containerRef={callContainerRef} />
</Box>
);
}

View File

@@ -0,0 +1,177 @@
import React from 'react';
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
import { useAtom } from 'jotai';
import * as css from './styles.css';
import { callChatAtom } from '../../state/callEmbed';
export function ControlDivider() {
return (
<Line variant="SurfaceVariant" size="300" direction="Vertical" className={css.ControlDivider} />
);
}
type MicrophoneButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon size="400" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
type SoundButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Surface' : 'Warning'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon
size="400"
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
filled={!enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type VideoButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon
size="400"
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
filled={enabled}
/>
</IconButton>
)}
</TooltipProvider>
);
}
type ScreenShareButtonProps = {
enabled: boolean;
onToggle: () => void;
};
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={enabled ? 'Success' : 'Surface'}
fill="Soft"
radii="400"
size="400"
onClick={() => onToggle()}
outlined
>
<Icon size="400" src={Icons.ScreenShare} filled={enabled} />
</IconButton>
)}
</TooltipProvider>
);
}
export function ChatButton() {
const [chat, setChat] = useAtom(callChatAtom);
return (
<TooltipProvider
position="Top"
delay={500}
tooltip={
<Tooltip>
<Text size="T200">{chat ? 'Close Chat' : 'Open Chat'}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant={chat ? 'Success' : 'Surface'}
fill="Soft"
radii="400"
size="400"
onClick={() => setChat(!chat)}
outlined
>
<Icon size="400" src={Icons.Message} filled={chat} />
</IconButton>
)}
</TooltipProvider>
);
}

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton } from './Controls';
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
import { useCallPreferences } from '../../state/hooks/callPreferences';
type PrescreenControlsProps = {
canJoin?: boolean;
};
export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
const room = useRoom();
const callEmbed = useCallEmbed();
const callJoined = useCallJoined(callEmbed);
const direct = useIsDirectRoom();
const inOtherCall = callEmbed && callEmbed.roomId !== room.roomId;
const startCall = useCallStart(direct);
const joining = callEmbed?.roomId === room.roomId && !callJoined;
const disabled = inOtherCall || !canJoin;
const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } =
useCallPreferences();
return (
<SequenceCard
className={css.ControlCard}
variant="SurfaceVariant"
gap="400"
radii="500"
alignItems="Center"
justifyContent="SpaceBetween"
wrap="Wrap"
>
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
<MicrophoneButton enabled={microphone} onToggle={toggleMicrophone} />
<SoundButton enabled={sound} onToggle={toggleSound} />
</Box>
<ControlDivider />
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
<VideoButton enabled={video} onToggle={toggleVideo} />
<ChatButton />
</Box>
<Box grow="Yes" direction="Column">
<Button
variant={disabled ? 'Secondary' : 'Success'}
fill={disabled ? 'Soft' : 'Solid'}
onClick={() => startCall(room, { microphone, video, sound })}
disabled={disabled || joining}
before={
joining ? (
<Spinner variant="Success" fill="Solid" size="200" />
) : (
<Icon src={Icons.Phone} size="200" filled />
)
}
>
<Text size="B400">Join</Text>
</Button>
</Box>
</SequenceCard>
);
}

View File

@@ -0,0 +1,28 @@
import { style } from '@vanilla-extract/css';
import { config, toRem } from 'folds';
export const CallViewContent = style({
padding: config.space.S400,
paddingRight: 0,
minHeight: '100%',
});
export const ControlCard = style({
padding: config.space.S300,
});
export const ControlDivider = style({
height: toRem(24),
});
export const CallMemberCard = style({
padding: config.space.S300,
});
export const CallControlContainer = style({
padding: config.space.S400,
});
export const PrescreenMessage = style({
padding: config.space.S200,
});

View File

@@ -6,9 +6,8 @@ import { useAtomValue } from 'jotai';
import {
ExtendedJoinRules,
JoinRulesSwitcher,
useRoomJoinRuleIcon,
useJoinRuleIcons,
useRoomJoinRuleLabel,
useSpaceJoinRuleIcon,
} from '../../../components/JoinRulesSwitcher';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
@@ -75,8 +74,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
return r;
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
const icons = useRoomJoinRuleIcon();
const spaceIcons = useSpaceJoinRuleIcon();
const icons = useJoinRuleIcons(room.getType());
const labels = useRoomJoinRuleLabel();
const [submitState, submit] = useAsyncCallback(
@@ -137,7 +135,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
}
after={
<JoinRulesSwitcher
icons={room.isSpaceRoom() ? spaceIcons : icons}
icons={icons}
labels={labels}
rules={joinRules}
value={rule}

View File

@@ -199,7 +199,7 @@ export function RoomProfileEdit({
alt={name}
renderFallback={() => (
<RoomIcon
space={room.isSpaceRoom()}
roomType={room.getType()}
size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
filled
@@ -342,7 +342,7 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
alt={name}
renderFallback={() => (
<RoomIcon
space={room.isSpaceRoom()}
roomType={room.getType()}
size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
filled

View File

@@ -1,5 +1,5 @@
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import { MatrixError, Room } from 'matrix-js-sdk';
import { MatrixError, Room, JoinRule } from 'matrix-js-sdk';
import {
Box,
Button,
@@ -33,24 +33,43 @@ import {
createRoom,
CreateRoomAliasInput,
CreateRoomData,
CreateRoomKind,
CreateRoomKindSelector,
CreateRoomAccess,
CreateRoomAccessSelector,
RoomVersionSelector,
useAdditionalCreators,
CreateRoomType,
} from '../../components/create-room';
import { RoomType } from '../../../types/matrix/room';
import { CreateRoomTypeSelector } from '../../components/create-room/CreateRoomTypeSelector';
import { getRoomIconSrc } from '../../utils/room';
const getCreateRoomKindToIcon = (kind: CreateRoomKind) => {
if (kind === CreateRoomKind.Private) return Icons.HashLock;
if (kind === CreateRoomKind.Restricted) return Icons.Hash;
return Icons.HashGlobe;
const getCreateRoomAccessToIcon = (access: CreateRoomAccess, type?: CreateRoomType) => {
const isVoiceRoom = type === CreateRoomType.VoiceRoom;
let joinRule: JoinRule = JoinRule.Public;
if (access === CreateRoomAccess.Restricted) joinRule = JoinRule.Restricted;
if (access === CreateRoomAccess.Private) joinRule = JoinRule.Knock;
return getRoomIconSrc(Icons, isVoiceRoom ? RoomType.Call : undefined, joinRule);
};
const getCreateRoomTypeToIcon = (type: CreateRoomType) => {
if (type === CreateRoomType.VoiceRoom) return Icons.VolumeHigh;
return Icons.Hash;
};
type CreateRoomFormProps = {
defaultKind?: CreateRoomKind;
defaultAccess?: CreateRoomAccess;
defaultType?: CreateRoomType;
space?: Room;
onCreate?: (roomId: string) => void;
};
export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormProps) {
export function CreateRoomForm({
defaultAccess,
defaultType,
space,
onCreate,
}: CreateRoomFormProps) {
const mx = useMatrixClient();
const alive = useAlive();
@@ -64,8 +83,9 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
const [kind, setKind] = useState(
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
const [type, setType] = useState(defaultType ?? CreateRoomType.TextRoom);
const [access, setAccess] = useState(
defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
);
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
@@ -75,13 +95,13 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false);
const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion);
const allowKnockRestricted =
kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion);
const handleRoomVersionChange = (version: string) => {
if (!restrictedSupported(version)) {
setKind(CreateRoomKind.Private);
setAccess(CreateRoomAccess.Private);
}
selectRoomVersion(version);
};
@@ -107,19 +127,23 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
if (!roomName) return;
const publicRoom = kind === CreateRoomKind.Public;
const publicRoom = access === CreateRoomAccess.Public;
let roomKnock = false;
if (allowKnock && kind === CreateRoomKind.Private) {
if (allowKnock && access === CreateRoomAccess.Private) {
roomKnock = knock;
}
if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
if (allowKnockRestricted && access === CreateRoomAccess.Restricted) {
roomKnock = knock;
}
let roomType: RoomType | undefined;
if (type === CreateRoomType.VoiceRoom) roomType = RoomType.Call;
create({
version: selectedRoomVersion,
type: roomType,
parent: space,
kind,
access,
name: roomName,
topic: roomTopic || undefined,
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
@@ -136,21 +160,32 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
return (
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
{!space && (
<Box direction="Column" gap="100">
<Text size="L400">Type</Text>
<CreateRoomTypeSelector
value={type}
onSelect={setType}
disabled={disabled}
getIcon={getCreateRoomTypeToIcon}
/>
</Box>
)}
<Box direction="Column" gap="100">
<Text size="L400">Access</Text>
<CreateRoomKindSelector
value={kind}
onSelect={setKind}
<CreateRoomAccessSelector
value={access}
onSelect={setAccess}
canRestrict={allowRestricted}
disabled={disabled}
getIcon={getCreateRoomKindToIcon}
getIcon={(roomAccess) => getCreateRoomAccessToIcon(roomAccess, type)}
/>
</Box>
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Name</Text>
<Input
required
before={<Icon size="100" src={getCreateRoomKindToIcon(kind)} />}
before={<Icon size="100" src={getCreateRoomAccessToIcon(access, type)} />}
name="nameInput"
autoFocus
size="500"
@@ -171,7 +206,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
/>
</Box>
{kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
{access === CreateRoomAccess.Public && <CreateRoomAliasInput disabled={disabled} />}
<Box shrink="No" direction="Column" gap="100">
<Box gap="200" alignItems="End">
@@ -183,7 +218,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
onClick={() => setAdvance(!advance)}
type="button"
>
<Text size="T200">Advance Options</Text>
<Text size="T200">Advanced Options</Text>
</Chip>
</Box>
</Box>
@@ -201,7 +236,7 @@ export function CreateRoomForm({ defaultKind, space, onCreate }: CreateRoomFormP
/>
</SequenceCard>
)}
{kind !== CreateRoomKind.Public && (
{access !== CreateRoomAccess.Public && (
<>
<SequenceCard
style={{ padding: config.space.S300 }}

View File

@@ -23,12 +23,13 @@ import {
} from '../../state/hooks/createRoomModal';
import { CreateRoomModalState } from '../../state/createRoomModal';
import { stopPropagation } from '../../utils/keyboard';
import { CreateRoomType } from '../../components/create-room/types';
type CreateRoomModalProps = {
state: CreateRoomModalState;
};
function CreateRoomModal({ state }: CreateRoomModalProps) {
const { spaceId } = state;
const { spaceId, type } = state;
const closeDialog = useCloseCreateRoomModal();
const allJoinedRooms = useAllJoinedRoomsSet();
@@ -57,7 +58,9 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
}}
>
<Box grow="Yes">
<Text size="H4">New Room</Text>
<Text size="H4">
{type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'}
</Text>
</Box>
<Box shrink="No">
<IconButton size="300" radii="300" onClick={closeDialog}>
@@ -74,7 +77,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
direction="Column"
gap="500"
>
<CreateRoomForm space={space} onCreate={closeDialog} />
<CreateRoomForm space={space} onCreate={closeDialog} defaultType={type} />
</Box>
</Scroll>
</Box>

View File

@@ -33,25 +33,25 @@ import {
createRoom,
CreateRoomAliasInput,
CreateRoomData,
CreateRoomKind,
CreateRoomKindSelector,
CreateRoomAccess,
CreateRoomAccessSelector,
RoomVersionSelector,
useAdditionalCreators,
} from '../../components/create-room';
import { RoomType } from '../../../types/matrix/room';
const getCreateSpaceKindToIcon = (kind: CreateRoomKind) => {
if (kind === CreateRoomKind.Private) return Icons.SpaceLock;
if (kind === CreateRoomKind.Restricted) return Icons.Space;
const getCreateSpaceAccessToIcon = (access: CreateRoomAccess) => {
if (access === CreateRoomAccess.Private) return Icons.SpaceLock;
if (access === CreateRoomAccess.Restricted) return Icons.Space;
return Icons.SpaceGlobe;
};
type CreateSpaceFormProps = {
defaultKind?: CreateRoomKind;
defaultAccess?: CreateRoomAccess;
space?: Room;
onCreate?: (roomId: string) => void;
};
export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFormProps) {
export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceFormProps) {
const mx = useMatrixClient();
const alive = useAlive();
@@ -65,8 +65,8 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
const allowRestricted = space && restrictedSupported(selectedRoomVersion);
const [kind, setKind] = useState(
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
const [access, setAccess] = useState(
defaultAccess ?? (allowRestricted ? CreateRoomAccess.Restricted : CreateRoomAccess.Private)
);
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
@@ -76,13 +76,13 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false);
const allowKnock = kind === CreateRoomKind.Private && knockSupported(selectedRoomVersion);
const allowKnock = access === CreateRoomAccess.Private && knockSupported(selectedRoomVersion);
const allowKnockRestricted =
kind === CreateRoomKind.Restricted && knockRestrictedSupported(selectedRoomVersion);
access === CreateRoomAccess.Restricted && knockRestrictedSupported(selectedRoomVersion);
const handleRoomVersionChange = (version: string) => {
if (!restrictedSupported(version)) {
setKind(CreateRoomKind.Private);
setAccess(CreateRoomAccess.Private);
}
selectRoomVersion(version);
};
@@ -108,12 +108,12 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
aliasInput && aliasInput.value ? replaceSpaceWithDash(aliasInput.value) : undefined;
if (!roomName) return;
const publicRoom = kind === CreateRoomKind.Public;
const publicRoom = access === CreateRoomAccess.Public;
let roomKnock = false;
if (allowKnock && kind === CreateRoomKind.Private) {
if (allowKnock && access === CreateRoomAccess.Private) {
roomKnock = knock;
}
if (allowKnockRestricted && kind === CreateRoomKind.Restricted) {
if (allowKnockRestricted && access === CreateRoomAccess.Restricted) {
roomKnock = knock;
}
@@ -121,7 +121,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
version: selectedRoomVersion,
type: RoomType.Space,
parent: space,
kind,
access,
name: roomName,
topic: roomTopic || undefined,
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
@@ -139,19 +139,19 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
<Box direction="Column" gap="100">
<Text size="L400">Access</Text>
<CreateRoomKindSelector
value={kind}
onSelect={setKind}
<CreateRoomAccessSelector
value={access}
onSelect={setAccess}
canRestrict={allowRestricted}
disabled={disabled}
getIcon={getCreateSpaceKindToIcon}
getIcon={getCreateSpaceAccessToIcon}
/>
</Box>
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Name</Text>
<Input
required
before={<Icon size="100" src={getCreateSpaceKindToIcon(kind)} />}
before={<Icon size="100" src={getCreateSpaceAccessToIcon(access)} />}
name="nameInput"
autoFocus
size="500"
@@ -172,7 +172,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
/>
</Box>
{kind === CreateRoomKind.Public && <CreateRoomAliasInput disabled={disabled} />}
{access === CreateRoomAccess.Public && <CreateRoomAliasInput disabled={disabled} />}
<Box shrink="No" direction="Column" gap="100">
<Box gap="200" alignItems="End">
@@ -184,7 +184,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
onClick={() => setAdvance(!advance)}
type="button"
>
<Text size="T200">Advance Options</Text>
<Text size="T200">Advanced Options</Text>
</Chip>
</Box>
</Box>
@@ -202,7 +202,7 @@ export function CreateSpaceForm({ defaultKind, space, onCreate }: CreateSpaceFor
/>
</SequenceCard>
)}
{kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
{access !== CreateRoomAccess.Public && advance && (allowKnock || allowKnockRestricted) && (
<SequenceCard
style={{ padding: config.space.S300 }}
variant="SurfaceVariant"

View File

@@ -165,7 +165,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
<Box shrink="No">
<BackRouteHandler>
{(onBack) => (
<IconButton onClick={onBack}>
<IconButton fill="None" onClick={onBack}>
<Icon src={Icons.ArrowLeft} />
</IconButton>
)}
@@ -218,7 +218,11 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
}
>
{(triggerRef) => (
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
<IconButton
fill="None"
ref={triggerRef}
onClick={() => setPeopleDrawer((drawer) => !drawer)}
>
<Icon size="400" src={Icons.User} />
</IconButton>
)}
@@ -235,7 +239,12 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
}
>
{(triggerRef) => (
<IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
<IconButton
fill="None"
onClick={handleOpenMenu}
ref={triggerRef}
aria-pressed={!!menuAnchor}
>
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
</IconButton>
)}

View File

@@ -175,6 +175,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
type RoomProfileProps = {
roomId: string;
roomType?: string;
name: string;
topic?: string;
avatarUrl?: string;
@@ -185,6 +186,7 @@ type RoomProfileProps = {
};
function RoomProfile({
roomId,
roomType,
name,
topic,
avatarUrl,
@@ -200,9 +202,7 @@ function RoomProfile({
roomId={roomId}
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon size="300" joinRule={joinRule ?? JoinRule.Restricted} filled />
)}
renderFallback={() => <RoomIcon size="300" joinRule={joinRule} roomType={roomType} />}
/>
</Avatar>
<Box grow="Yes" direction="Column">
@@ -338,6 +338,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
{(localSummary) => (
<RoomProfile
roomId={roomId}
roomType={localSummary.roomType}
name={localSummary.name}
topic={localSummary.topic}
avatarUrl={
@@ -396,6 +397,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
{summary && (
<RoomProfile
roomId={roomId}
roomType={summary.room_type}
name={summary.name || summary.canonical_alias || roomId}
topic={summary.topic}
avatarUrl={

View File

@@ -36,6 +36,8 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
import { AddExistingModal } from '../add-existing';
import { CreateRoomType } from '../../components/create-room/types';
import { BetaNoticeBadge } from '../../components/BetaNoticeBadge';
function SpaceProfileLoading() {
return (
@@ -249,8 +251,8 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
setCords(evt.currentTarget.getBoundingClientRect());
};
const handleCreateRoom = () => {
openCreateRoomModal(item.roomId);
const handleCreateRoom = (type?: CreateRoomType) => {
openCreateRoomModal(item.roomId, type);
setCords(undefined);
};
@@ -281,9 +283,19 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
radii="300"
variant="Primary"
fill="None"
onClick={handleCreateRoom}
onClick={() => handleCreateRoom(CreateRoomType.TextRoom)}
>
<Text size="T300">New Room</Text>
<Text size="T300">Chat Room</Text>
</MenuItem>
<MenuItem
size="300"
radii="300"
variant="Primary"
fill="None"
onClick={() => handleCreateRoom(CreateRoomType.VoiceRoom)}
after={<BetaNoticeBadge />}
>
<Text size="T300">Voice Room</Text>
</MenuItem>
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
<Text size="T300">Existing Room</Text>

View File

@@ -29,7 +29,7 @@ import { SearchOrderBy } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { joinRuleToIconSrc } from '../../utils/room';
import { getRoomIconSrc } from '../../utils/room';
import { factoryRoomIdByAtoZ } from '../../utils/sort';
import {
SearchItemStrGetter,
@@ -274,9 +274,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
before={
<Icon
size="50"
src={
joinRuleToIconSrc(Icons, room.getJoinRule(), false) ?? Icons.Hash
}
src={getRoomIconSrc(Icons, room.getType(), room.getJoinRule())}
/>
}
>
@@ -392,10 +390,7 @@ export function SearchFilters({
onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))}
radii="Pill"
before={
<Icon
size="50"
src={joinRuleToIconSrc(Icons, room.getJoinRule(), false) ?? Icons.Hash}
/>
<Icon size="50" src={getRoomIconSrc(Icons, room.getType(), room.getJoinRule())} />
}
after={<Icon size="50" src={Icons.Cross} />}
>

View File

@@ -203,7 +203,12 @@ export function SearchResultGroup({
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name}
renderFallback={() => (
<RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
<RoomIcon
size="50"
roomType={room.getType()}
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
filled
/>
)}
/>
</Avatar>

View File

@@ -19,6 +19,7 @@ import {
} from 'folds';
import { useFocusWithin, useHover } from 'react-aria';
import FocusTrap from 'focus-trap-react';
import { useAtom, useAtomValue } from 'jotai';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
@@ -51,6 +52,13 @@ import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationS
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { useRoomName } from '../../hooks/useRoomMeta';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
import { callChatAtom } from '../../state/callEmbed';
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '../../hooks/useLivekitSupport';
type RoomNavItemMenuProps = {
room: Room;
@@ -209,6 +217,24 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
}
);
function CallChatToggle() {
const [chat, setChat] = useAtom(callChatAtom);
return (
<IconButton
onClick={() => setChat(!chat)}
aria-pressed={chat}
aria-label="Toggle Chat"
variant="Background"
fill="None"
size="300"
radii="300"
>
<Icon size="50" src={Icons.Message} filled={chat} />
</IconButton>
);
}
type RoomNavItemProps = {
room: Room;
selected: boolean;
@@ -236,6 +262,8 @@ export function RoomNavItem({
(receipt) => receipt.userId !== mx.getUserId()
);
const roomName = useRoomName(room);
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
evt.preventDefault();
setMenuAnchor({
@@ -251,6 +279,29 @@ export function RoomNavItem({
};
const optionsVisible = hover || !!menuAnchor;
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const startCall = useCallStart(direct);
const callEmbed = useCallEmbed();
const callPref = useAtomValue(useCallPreferencesAtom());
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
// Do not join if no livekit support or call is not started by others
if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) {
return;
}
// Do not join if already in call
if (callEmbed) {
return;
}
// Start call in second click
if (selected) {
evt.preventDefault();
startCall(room, callPref);
}
};
return (
<NavItem
@@ -263,7 +314,7 @@ export function RoomNavItem({
{...hoverProps}
{...focusWithinProps}
>
<NavLink to={linkPath}>
<NavLink to={linkPath} onClick={room.isCallRoom() ? handleStartCall : undefined}>
<NavItemContent>
<Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400">
@@ -275,25 +326,28 @@ export function RoomNavItem({
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
}
alt={room.name}
alt={roomName}
renderFallback={() => (
<Text as="span" size="H6">
{nameInitials(room.name)}
{nameInitials(roomName)}
</Text>
)}
/>
) : (
<RoomIcon
style={{ opacity: unread ? config.opacity.P500 : config.opacity.P300 }}
style={{
opacity: unread ? config.opacity.P500 : config.opacity.P300,
}}
filled={selected}
size="100"
joinRule={room.getJoinRule()}
roomType={room.getType()}
/>
)}
</Avatar>
<Box as="span" grow="Yes">
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
{room.name}
{roomName}
</Text>
</Box>
{!optionsVisible && !unread && !selected && typingMember.length > 0 && (
@@ -307,14 +361,30 @@ export function RoomNavItem({
</UnreadBadgeCenter>
)}
{!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
<Icon size="50" src={getRoomNotificationModeIcon(notificationMode)} />
<Icon
size="50"
src={getRoomNotificationModeIcon(notificationMode)}
aria-label={notificationMode}
/>
)}
{room.isCallRoom() && callMembers.length > 0 && (
<Badge variant="Critical" fill="Solid" size="400">
<Text as="span" size="L400" truncate>
{callMembers.length} Live
</Text>
</Badge>
)}
</Box>
</NavItemContent>
</NavLink>
{optionsVisible && (
<NavItemOptions>
{selected && (callEmbed?.roomId === room.roomId || room.isCallRoom()) && (
<CallChatToggle />
)}
<PopOut
id={`menu-${room.roomId}`}
aria-expanded={!!menuAnchor}
anchor={menuAnchor}
offset={menuAnchor?.width === 0 ? 0 : undefined}
alignOffset={menuAnchor?.width === 0 ? 0 : -5}
@@ -343,6 +413,8 @@ export function RoomNavItem({
<IconButton
onClick={handleOpenMenu}
aria-pressed={!!menuAnchor}
aria-controls={`menu-${room.roomId}`}
aria-label="More Options"
variant="Background"
fill="None"
size="300"

View File

@@ -104,6 +104,7 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
renderFallback={() => (
<RoomIcon
size="50"
roomType={room.getType()}
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
filled
/>

View File

@@ -59,7 +59,7 @@ export function General({ requestClose }: GeneralProps) {
<RoomLocalAddresses permissions={permissions} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text>
<Text size="L400">Advanced Options</Text>
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box>
</Box>

View File

@@ -23,7 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups();
const permissionGroups = usePermissionGroups(room.isCallRoom());
const [powerEditor, setPowerEditor] = useState(false);

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
import { PermissionGroup } from '../../common-settings/permissions';
export const usePermissionGroups = (): PermissionGroup[] => {
export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
const groups: PermissionGroup[] = useMemo(() => {
const messagesGroup: PermissionGroup = {
name: 'Messages',
@@ -46,6 +46,19 @@ export const usePermissionGroups = (): PermissionGroup[] => {
],
};
const callSettingsGroup: PermissionGroup = {
name: 'Calls',
items: [
{
location: {
state: true,
key: StateEvent.GroupCallMemberPrefix,
},
name: 'Join Call',
},
],
};
const moderationGroup: PermissionGroup = {
name: 'Moderation',
items: [
@@ -177,6 +190,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
const otherSettingsGroup: PermissionGroup = {
name: 'Other',
items: [
{
location: {
state: true,
key: StateEvent.PoniesRoomEmotes,
},
name: 'Manage Emojis & Stickers',
},
{
location: {
state: true,
@@ -196,12 +216,13 @@ export const usePermissionGroups = (): PermissionGroup[] => {
return [
messagesGroup,
...(isCallRoom ? [callSettingsGroup] : []),
moderationGroup,
roomOverviewGroup,
roomSettingsGroup,
otherSettingsGroup,
];
}, []);
}, [isCallRoom]);
return groups;
};

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { useSetAtom } from 'jotai';
import { useParams } from 'react-router-dom';
import { Box, Text, TooltipProvider, Tooltip, Icon, Icons, IconButton, toRem } from 'folds';
import { Page, PageHeader } from '../../components/page';
import { callChatAtom } from '../../state/callEmbed';
import { RoomView } from './RoomView';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
export function CallChatView() {
const { eventId } = useParams();
const setChat = useSetAtom(callChatAtom);
const screenSize = useScreenSizeContext();
const handleClose = () => setChat(false);
return (
<Page
style={{
width: screenSize === ScreenSize.Desktop ? toRem(456) : '100%',
flexShrink: 0,
flexGrow: 0,
}}
>
<PageHeader>
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes">
<Text size="H5" truncate>
Chat
</Text>
</Box>
<Box shrink="No" alignItems="Center">
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>Close</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton ref={triggerRef} variant="Surface" onClick={handleClose}>
<Icon src={Icons.Cross} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
</PageHeader>
<Box grow="Yes" direction="Column">
<RoomView eventId={eventId} />
</Box>
</Page>
);
}

View File

@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import { Box, Line } from 'folds';
import { useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey';
import { useAtomValue } from 'jotai';
import { RoomView } from './RoomView';
import { MembersDrawer } from './MembersDrawer';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
@@ -13,6 +14,10 @@ import { useKeyDown } from '../../hooks/useKeyDown';
import { markAsRead } from '../../utils/notifications';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers';
import { CallView } from '../call/CallView';
import { RoomViewHeader } from './RoomViewHeader';
import { callChatAtom } from '../../state/callEmbed';
import { CallChatView } from './CallChatView';
export function Room() {
const { eventId } = useParams();
@@ -24,6 +29,7 @@ export function Room() {
const screenSize = useScreenSizeContext();
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
const chat = useAtomValue(callChatAtom);
useKeyDown(
window,
@@ -37,11 +43,37 @@ export function Room() {
)
);
const callView = room.isCallRoom();
return (
<PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes">
<RoomView room={room} eventId={eventId} />
{screenSize === ScreenSize.Desktop && isDrawer && (
{callView && (screenSize === ScreenSize.Desktop || !chat) && (
<Box grow="Yes" direction="Column">
<RoomViewHeader callView />
<Box grow="Yes">
<CallView />
</Box>
</Box>
)}
{!callView && (
<Box grow="Yes" direction="Column">
<RoomViewHeader />
<Box grow="Yes">
<RoomView eventId={eventId} />
</Box>
</Box>
)}
{callView && chat && (
<>
{screenSize === ScreenSize.Desktop && (
<Line variant="Background" direction="Vertical" size="300" />
)}
<CallChatView />
</>
)}
{!callView && screenSize === ScreenSize.Desktop && isDrawer && (
<>
<Line variant="Background" direction="Vertical" size="300" />
<MembersDrawer key={room.roomId} room={room} members={members} />

View File

@@ -221,7 +221,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const isComposing = useComposingCheck();
useElementSizeObserver(
useCallback(() => document.body, []),
useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]),
useCallback((width) => setHideStickerBtn(width < 500), [])
);

View File

@@ -27,6 +27,7 @@ import { HTMLReactParserOptions } from 'html-react-parser';
import classNames from 'classnames';
import { ReactEditor } from 'slate-react';
import { Editor } from 'slate';
import { SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import to from 'await-to-js';
import { useAtomValue, useSetAtom } from 'jotai';
import {
@@ -471,6 +472,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const permissions = useRoomPermissions(creators, powerLevels);
const canRedact = permissions.action('redact', mx.getSafeUserId());
const canDeleteOwn = permissions.event(MessageEvent.RoomRedaction, mx.getSafeUserId());
const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
const [editId, setEditId] = useState<string>();
@@ -1047,7 +1049,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1129,7 +1131,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
collapse={collapse}
highlight={highlighted}
edit={editId === mEventId}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1247,7 +1249,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
messageLayout={messageLayout}
collapse={collapse}
highlight={highlighted}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
@@ -1468,6 +1470,57 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
</Event>
);
},
[StateEvent.GroupCallMemberPrefix]: (mEventId, mEvent, item) => {
const highlighted = focusItem?.index === item && focusItem.highlight;
const senderId = mEvent.getSender() ?? '';
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
const content = mEvent.getContent<SessionMembershipData>();
const prevContent = mEvent.getPrevContent();
const callJoined = content.application;
if (callJoined && 'application' in prevContent) {
return null;
}
const timeJSX = (
<Time
ts={mEvent.getTs()}
compact={messageLayout === MessageLayout.Compact}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
);
return (
<Event
key={mEvent.getId()}
data-message-item={item}
data-message-id={mEventId}
room={room}
mEvent={mEvent}
highlight={highlighted}
messageSpacing={messageSpacing}
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
>
<EventContent
messageLayout={messageLayout}
time={timeJSX}
iconSrc={callJoined ? Icons.Phone : Icons.PhoneDown}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<b>{senderName}</b>
{callJoined ? ' joined the call' : ' ended the call'}
</Text>
</Box>
}
/>
</Event>
);
},
},
(mEventId, mEvent, item) => {
if (!showHiddenEvents) return null;

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useRef } from 'react';
import { Box, Text, config } from 'folds';
import { EventType, Room } from 'matrix-js-sdk';
import { EventType } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react';
import { isKeyHotkey } from 'is-hotkey';
import { useStateEvent } from '../../hooks/useStateEvent';
@@ -15,13 +15,13 @@ import { RoomTombstone } from './RoomTombstone';
import { RoomInput } from './RoomInput';
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
import { Page } from '../../components/page';
import { RoomViewHeader } from './RoomViewHeader';
import { useKeyDown } from '../../hooks/useKeyDown';
import { editableActiveElement } from '../../utils/dom';
import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoom } from '../../hooks/useRoom';
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
@@ -30,10 +30,8 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return false;
}
// do not focus on F keys
if (FN_KEYS_REGEX.test(code)) return false;
// do not focus on numlock/scroll lock
if (
code.startsWith('OS') ||
code.startsWith('Meta') ||
@@ -56,12 +54,13 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return true;
};
export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
export function RoomView({ eventId }: { eventId?: string }) {
const roomInputRef = useRef<HTMLDivElement>(null);
const roomViewRef = useRef<HTMLDivElement>(null);
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const room = useRoom();
const { roomId } = room;
const editor = useEditor();
@@ -93,7 +92,6 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
return (
<Page ref={roomViewRef}>
<RoomViewHeader />
<Box grow="Yes" direction="Column">
<RoomTimeline
key={roomId}

View File

@@ -23,9 +23,7 @@ import {
Spinner,
} from 'folds';
import { useNavigate } from 'react-router-dom';
import { JoinRule, Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
import { Room } from 'matrix-js-sdk';
import { useStateEvent } from '../../hooks/useStateEvent';
import { PageHeader } from '../../components/page';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
@@ -33,7 +31,7 @@ import { UseStateProvider } from '../../components/UseStateProvider';
import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { StateEvent } from '../../../types/matrix/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoom } from '../../hooks/useRoom';
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace';
@@ -48,7 +46,6 @@ import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { copyToClipboard } from '../../utils/dom';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to';
@@ -69,6 +66,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { RoomSettingsPage } from '../../state/roomSettings';
type RoomMenuProps = {
room: Room;
@@ -254,7 +253,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
);
});
export function RoomViewHeader() {
export function RoomViewHeader({ callView }: { callView?: boolean }) {
const navigate = useNavigate();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@@ -263,12 +262,12 @@ export function RoomViewHeader() {
const space = useSpaceOptionally();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
const mDirects = useAtomValue(mDirectAtom);
const direct = useIsDirectRoom();
const pinnedEvents = useRoomPinnedEvents(room);
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
const ecryptedRoom = !!encryptionEvent;
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const encryptedRoom = !!encryptionEvent;
const avatarMxc = useRoomAvatar(room, direct);
const name = useRoomName(room);
const topic = useRoomTopic(room);
const avatarUrl = avatarMxc
@@ -295,14 +294,27 @@ export function RoomViewHeader() {
setPinMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
const openSettings = useOpenRoomSettings();
const parentSpace = useSpaceOptionally();
const handleMemberToggle = () => {
if (callView) {
openSettings(room.roomId, parentSpace?.roomId, RoomSettingsPage.MembersPage);
return;
}
setPeopleDrawer(!peopleDrawer);
};
return (
<PageHeader balance={screenSize === ScreenSize.Mobile}>
<PageHeader
className={ContainerColor({ variant: 'Surface' })}
balance={screenSize === ScreenSize.Mobile}
>
<Box grow="Yes" gap="300">
{screenSize === ScreenSize.Mobile && (
<BackRouteHandler>
{(onBack) => (
<Box shrink="No" alignItems="Center">
<IconButton onClick={onBack}>
<IconButton fill="None" onClick={onBack}>
<Icon src={Icons.ArrowLeft} />
</IconButton>
</Box>
@@ -317,11 +329,7 @@ export function RoomViewHeader() {
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon
size="200"
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
filled
/>
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} />
)}
/>
</Avatar>
@@ -369,8 +377,9 @@ export function RoomViewHeader() {
)}
</Box>
</Box>
<Box shrink="No">
{!ecryptedRoom && (
{!encryptedRoom && (
<TooltipProvider
position="Bottom"
offset={4}
@@ -381,7 +390,7 @@ export function RoomViewHeader() {
}
>
{(triggerRef) => (
<IconButton ref={triggerRef} onClick={handleSearchClick}>
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick}>
<Icon size="400" src={Icons.Search} />
</IconButton>
)}
@@ -398,6 +407,7 @@ export function RoomViewHeader() {
>
{(triggerRef) => (
<IconButton
fill="None"
style={{ position: 'relative' }}
onClick={handleOpenPinMenu}
ref={triggerRef}
@@ -443,23 +453,29 @@ export function RoomViewHeader() {
</FocusTrap>
}
/>
{screenSize === ScreenSize.Desktop && (
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
{callView ? (
<Text>Members</Text>
) : (
<Text>{peopleDrawer ? 'Hide Members' : 'Show Members'}</Text>
)}
</Tooltip>
}
>
{(triggerRef) => (
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
<IconButton fill="None" ref={triggerRef} onClick={handleMemberToggle}>
<Icon size="400" src={Icons.User} />
</IconButton>
)}
</TooltipProvider>
)}
<TooltipProvider
position="Bottom"
align="End"
@@ -471,7 +487,12 @@ export function RoomViewHeader() {
}
>
{(triggerRef) => (
<IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
<IconButton
fill="None"
onClick={handleOpenMenu}
ref={triggerRef}
aria-pressed={!!menuAnchor}
>
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
</IconButton>
)}

View File

@@ -327,11 +327,9 @@ export const MessageCopyLinkItem = as<
const mx = useMatrixClient();
const handleCopy = () => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
const eventId = mEvent.getId();
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
if (!eventId) return;
copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
copyToClipboard(getMatrixToRoomEvent(room.roomId, eventId, getViaServers(room)));
onClose?.();
};

View File

@@ -286,7 +286,7 @@ export function Search({ requestClose }: SearchProps) {
gap="100"
>
<Text size="H6" align="Center">
{result ? 'No Match Found' : `No Rooms'}`}
{result ? 'No Match Found' : 'No Rooms'}
</Text>
<Text size="T200" align="Center">
{result
@@ -373,7 +373,7 @@ export function Search({ requestClose }: SearchProps) {
<RoomIcon
size="100"
joinRule={room.getJoinRule()}
space={room.isSpaceRoom()}
roomType={room.getType()}
/>
)}
</Avatar>

View File

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

View File

@@ -103,7 +103,7 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
alt={roomName}
renderFallback={() => (
<RoomIcon
space
roomType={room.getType()}
size="50"
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
filled

View File

@@ -55,7 +55,7 @@ export function General({ requestClose }: GeneralProps) {
<RoomLocalAddresses permissions={permissions} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text>
<Text size="L400">Advanced Options</Text>
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box>
</Box>

Some files were not shown because too many files have changed in this diff Show More