Merge branch 'develop' into travis/pinned-room-list

This commit is contained in:
Travis Ralston 2018-10-12 14:09:52 -06:00
commit 103ed71eb5
284 changed files with 17463 additions and 12849 deletions

View file

@ -2,9 +2,7 @@
src/autocomplete/AutocompleteProvider.js
src/autocomplete/Autocompleter.js
src/autocomplete/EmojiProvider.js
src/autocomplete/UserProvider.js
src/CallHandler.js
src/component-index.js
src/components/structures/BottomLeftMenu.js
src/components/structures/CompatibilityPage.js
@ -13,27 +11,22 @@ src/components/structures/HomePage.js
src/components/structures/LeftPanel.js
src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js
src/components/structures/login/Login.js
src/components/structures/login/Registration.js
src/components/structures/LoginBox.js
src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js
src/components/structures/RoomDirectory.js
src/components/structures/RoomStatusBar.js
src/components/structures/RoomSubList.js
src/components/structures/RoomView.js
src/components/structures/ScrollPanel.js
src/components/structures/SearchBox.js
src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js
src/components/structures/UserSettings.js
src/components/structures/ViewSource.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/GroupAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/BugReportDialog.js
src/components/views/dialogs/ChangelogDialog.js
src/components/views/dialogs/ChatCreateOrReuseDialog.js
src/components/views/dialogs/DeactivateAccountDialog.js
src/components/views/dialogs/SetPasswordDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
@ -41,7 +34,6 @@ src/components/views/directory/NetworkDropdown.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/EditableText.js
src/components/views/elements/ImageView.js
src/components/views/elements/InlineSpinner.js
src/components/views/elements/MemberEventListSummary.js
@ -61,11 +53,9 @@ src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js
src/components/views/room_settings/AliasSettings.js
src/components/views/room_settings/ColorSettings.js
src/components/views/room_settings/UrlPreviewSettings.js
src/components/views/rooms/Autocomplete.js
src/components/views/rooms/AuxPanel.js
src/components/views/rooms/EntityTile.js
src/components/views/rooms/EventTile.js
src/components/views/rooms/LinkPreviewWidget.js
src/components/views/rooms/MemberDeviceInfo.js
src/components/views/rooms/MemberInfo.js
@ -73,12 +63,10 @@ src/components/views/rooms/MemberList.js
src/components/views/rooms/MemberTile.js
src/components/views/rooms/MessageComposer.js
src/components/views/rooms/MessageComposerInput.js
src/components/views/rooms/RoomDropTarget.js
src/components/views/rooms/PinnedEventTile.js
src/components/views/rooms/RoomList.js
src/components/views/rooms/RoomPreviewBar.js
src/components/views/rooms/RoomSettings.js
src/components/views/rooms/RoomTile.js
src/components/views/rooms/RoomTooltip.js
src/components/views/rooms/SearchableEntityList.js
src/components/views/rooms/SearchBar.js
src/components/views/rooms/SearchResultTile.js
@ -86,12 +74,12 @@ src/components/views/rooms/TopUnreadMessagesBar.js
src/components/views/rooms/UserTile.js
src/components/views/settings/AddPhoneNumber.js
src/components/views/settings/ChangeAvatar.js
src/components/views/settings/ChangeDisplayName.js
src/components/views/settings/ChangePassword.js
src/components/views/settings/DevicesPanel.js
src/components/views/settings/IntegrationsManager.js
src/components/views/settings/Notifications.js
src/ContentMessages.js
src/GroupAddressPicker.js
src/HtmlUtils.js
src/ImageUtils.js
src/languageHandler.js
@ -101,7 +89,6 @@ src/Markdown.js
src/MatrixClientPeg.js
src/Modal.js
src/notifications/ContentRules.js
src/notifications/NotificationUtils.js
src/notifications/PushRuleVectorState.js
src/notifications/StandardActions.js
src/notifications/VectorPushRulesDefinitions.js
@ -111,7 +98,6 @@ src/Presence.js
src/rageshake/rageshake.js
src/rageshake/submit-rageshake.js
src/ratelimitedfunc.js
src/RichText.js
src/Roles.js
src/Rooms.js
src/ScalarAuthClient.js
@ -135,6 +121,7 @@ test/components/structures/TimelinePanel-test.js
test/components/views/dialogs/InteractiveAuthDialog-test.js
test/components/views/login/RegistrationForm-test.js
test/components/views/rooms/MessageComposerInput-test.js
test/components/views/rooms/RoomSettings-test.js
test/mock-clock.js
test/notifications/ContentRules-test.js
test/notifications/PushRuleVectorState-test.js

View file

@ -95,6 +95,7 @@ module.exports = {
"new-cap": ["warn"],
"key-spacing": ["warn"],
"prefer-const": ["warn"],
"arrow-parens": "off",
// crashes currently: https://github.com/eslint/eslint/issues/6274
"generator-star-spacing": "off",

3
.gitignore vendored
View file

@ -14,3 +14,6 @@ npm-debug.log
/src/component-index.js
.DS_Store
# https://github.com/vector-im/riot-web/issues/7083
package-lock.json

View file

@ -10,7 +10,7 @@ RIOT_WEB_DIR=riot-web
REACT_SDK_DIR=`pwd`
scripts/fetchdep.sh vector-im riot-web
cd "$RIOT_WEB_DIR"
pushd "$RIOT_WEB_DIR"
mkdir node_modules
npm install
@ -23,4 +23,16 @@ ln -s "$REACT_SDK_DIR/node_modules/matrix-js-sdk" node_modules/matrix-js-sdk
rm -r node_modules/matrix-react-sdk
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
npm run build
npm run test
popd
# run end to end tests
git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master
pushd matrix-react-end-to-end-tests
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
./install.sh
./run.sh --travis
popd

View file

@ -15,5 +15,7 @@ addons:
chrome: stable
install:
- npm install
# install synapse prerequisites for end to end tests
- sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev
script:
./scripts/travis.sh

View file

@ -1,3 +1,576 @@
Changes in [0.13.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.6) (2018-10-08)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.5...v0.13.6)
* Fix resuming session in Firefox private mode/Tor browser being broken
[\#2195](https://github.com/matrix-org/matrix-react-sdk/pull/2195)
* Show warning about using lazy-loading/non-lazy-loading versions simultaneously (/app & /develop)
[\#2201](https://github.com/matrix-org/matrix-react-sdk/pull/2201)
Changes in [0.13.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.5) (2018-10-01)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.5-rc.1...v0.13.5)
* No changes since rc.1
Changes in [0.13.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.5-rc.1) (2018-09-27)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.4...v0.13.5-rc.1)
* resync when LL is toggled, show message when enabled
[\#2178](https://github.com/matrix-org/matrix-react-sdk/pull/2178)
* Update from Weblate.
[\#2179](https://github.com/matrix-org/matrix-react-sdk/pull/2179)
* Split npm start into an init and watch script
[\#2175](https://github.com/matrix-org/matrix-react-sdk/pull/2175)
* show canonical aliases in timeline, and set/remove implicit ones
[\#2171](https://github.com/matrix-org/matrix-react-sdk/pull/2171)
* Fix stale RR and improve LL reliability in RoomView & MemberList.
[\#2168](https://github.com/matrix-org/matrix-react-sdk/pull/2168)
* pass --travis flag to e2e tests to disable tests known not to work Travis CI
[\#2170](https://github.com/matrix-org/matrix-react-sdk/pull/2170)
* Add m.room.aliases to the timeline
[\#2167](https://github.com/matrix-org/matrix-react-sdk/pull/2167)
* postpone loading the members until the user joined the room
[\#2165](https://github.com/matrix-org/matrix-react-sdk/pull/2165)
* Allow translation tags object to be a variable
[\#2166](https://github.com/matrix-org/matrix-react-sdk/pull/2166)
* Don't try to exit fullscreen if not fullscreen
[\#2164](https://github.com/matrix-org/matrix-react-sdk/pull/2164)
* avoid updating the memberlist while the spinner is shown
[\#2161](https://github.com/matrix-org/matrix-react-sdk/pull/2161)
* fix logging room id when LL members fail
[\#2163](https://github.com/matrix-org/matrix-react-sdk/pull/2163)
* dont keep the spinner in the memberlist when fetching /members fails
[\#2162](https://github.com/matrix-org/matrix-react-sdk/pull/2162)
* only dispatch an action for self-membership
[\#2160](https://github.com/matrix-org/matrix-react-sdk/pull/2160)
* avoid unneeded lookups in memberDict
[\#2153](https://github.com/matrix-org/matrix-react-sdk/pull/2153)
* Update from Weblate.
[\#2157](https://github.com/matrix-org/matrix-react-sdk/pull/2157)
* avoid memberlist refresh for events related to rooms other but the current
[\#2156](https://github.com/matrix-org/matrix-react-sdk/pull/2156)
Changes in [0.13.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.4) (2018-09-10)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.4-rc.1...v0.13.4)
* No changes since rc.1
Changes in [0.13.4-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.4-rc.1) (2018-09-07)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.3...v0.13.4-rc.1)
* Error on splash screen if sync is failing
[\#2155](https://github.com/matrix-org/matrix-react-sdk/pull/2155)
* Do full registration if HS doesn't support ILAG
[\#2150](https://github.com/matrix-org/matrix-react-sdk/pull/2150)
* Re-apply "Don't rely on room members to query power levels"
[\#2152](https://github.com/matrix-org/matrix-react-sdk/pull/2152)
* s/DidMount/WillMount/ in MessageComposerInput
[\#2151](https://github.com/matrix-org/matrix-react-sdk/pull/2151)
* Revert "Don't rely on room members to query power levels"
[\#2149](https://github.com/matrix-org/matrix-react-sdk/pull/2149)
* Don't rely on room members to query power levels
[\#2145](https://github.com/matrix-org/matrix-react-sdk/pull/2145)
* Correctly mark email as optional
[\#2148](https://github.com/matrix-org/matrix-react-sdk/pull/2148)
* guests trying to join communities should fire the ILAG flow.
[\#2059](https://github.com/matrix-org/matrix-react-sdk/pull/2059)
* Fix DM avatars, part 3
[\#2146](https://github.com/matrix-org/matrix-react-sdk/pull/2146)
* Fix: show spinner again while recovering from connection error
[\#2143](https://github.com/matrix-org/matrix-react-sdk/pull/2143)
* Fix: infinite spinner on trying to create welcomeUserId room without consent
[\#2147](https://github.com/matrix-org/matrix-react-sdk/pull/2147)
* Show spinner in member list while loading members
[\#2139](https://github.com/matrix-org/matrix-react-sdk/pull/2139)
* Slash command to discard megolm session
[\#2140](https://github.com/matrix-org/matrix-react-sdk/pull/2140)
Changes in [0.13.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.3) (2018-09-03)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.3-rc.2...v0.13.3)
* No changes since rc.2
Changes in [0.13.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.3-rc.2) (2018-08-31)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.3-rc.1...v0.13.3-rc.2)
* Update js-sdk to fix exception
Changes in [0.13.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.3-rc.1) (2018-08-30)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.2...v0.13.3-rc.1)
* Fix DM avatar
[\#2141](https://github.com/matrix-org/matrix-react-sdk/pull/2141)
* Update from Weblate.
[\#2142](https://github.com/matrix-org/matrix-react-sdk/pull/2142)
* Support m.room.tombstone events
[\#2124](https://github.com/matrix-org/matrix-react-sdk/pull/2124)
* Support room creation events
[\#2123](https://github.com/matrix-org/matrix-react-sdk/pull/2123)
* Support for room upgrades
[\#2122](https://github.com/matrix-org/matrix-react-sdk/pull/2122)
* Fix: dont show 1:1 avatar for rooms +2 members but only <=2 members loaded
[\#2137](https://github.com/matrix-org/matrix-react-sdk/pull/2137)
* Render terms & conditions in settings
[\#2136](https://github.com/matrix-org/matrix-react-sdk/pull/2136)
* Don't crash if the value of a room tag is null
[\#2133](https://github.com/matrix-org/matrix-react-sdk/pull/2133)
* Add stub for getVisibleRooms()
[\#2134](https://github.com/matrix-org/matrix-react-sdk/pull/2134)
* Fix LL crash trying to render own avatar in composer when member isn't
available yet
[\#2132](https://github.com/matrix-org/matrix-react-sdk/pull/2132)
* Support M_INCOMPATIBLE_ROOM_VERSION
[\#2125](https://github.com/matrix-org/matrix-react-sdk/pull/2125)
* Hide replaced rooms
[\#2127](https://github.com/matrix-org/matrix-react-sdk/pull/2127)
* Fix CPU spin on joining large room
[\#2128](https://github.com/matrix-org/matrix-react-sdk/pull/2128)
* Change format of server usage limit message
[\#2131](https://github.com/matrix-org/matrix-react-sdk/pull/2131)
* Re-apply "Fix showing peek preview while LL members are loading""
[\#2130](https://github.com/matrix-org/matrix-react-sdk/pull/2130)
* Revert "Fix showing peek preview while LL members are loading"
[\#2129](https://github.com/matrix-org/matrix-react-sdk/pull/2129)
* Fix showing peek preview while LL members are loading
[\#2126](https://github.com/matrix-org/matrix-react-sdk/pull/2126)
* Destroy non-persistent widgets when switching room
[\#2098](https://github.com/matrix-org/matrix-react-sdk/pull/2098)
* Lazy loading of room members
[\#2118](https://github.com/matrix-org/matrix-react-sdk/pull/2118)
* Lazy loading: feature toggle
[\#2115](https://github.com/matrix-org/matrix-react-sdk/pull/2115)
* Lazy loading: cleanup
[\#2116](https://github.com/matrix-org/matrix-react-sdk/pull/2116)
* Lazy loading: fix end-to-end encryption rooms
[\#2113](https://github.com/matrix-org/matrix-react-sdk/pull/2113)
* Lazy loading: Lazy load members while backpaginating
[\#2104](https://github.com/matrix-org/matrix-react-sdk/pull/2104)
* Lazy loading: don't assume we have our own member available
[\#2102](https://github.com/matrix-org/matrix-react-sdk/pull/2102)
* Lazy load room members - Part I
[\#2072](https://github.com/matrix-org/matrix-react-sdk/pull/2072)
Changes in [0.13.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.2) (2018-08-23)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.1...v0.13.2)
* Don't crash if the value of a room tag is null
[\#2135](https://github.com/matrix-org/matrix-react-sdk/pull/2135)
Changes in [0.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.1) (2018-08-20)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.1-rc.1...v0.13.1)
* No changes since rc.1
Changes in [0.13.1-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.1-rc.1) (2018-08-16)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.0...v0.13.1-rc.1)
* Update from Weblate.
[\#2121](https://github.com/matrix-org/matrix-react-sdk/pull/2121)
* Shift to M_RESOURCE_LIMIT_EXCEEDED errors
[\#2120](https://github.com/matrix-org/matrix-react-sdk/pull/2120)
* Fix RoomSettings test
[\#2119](https://github.com/matrix-org/matrix-react-sdk/pull/2119)
* Show room version number in room settings
[\#2117](https://github.com/matrix-org/matrix-react-sdk/pull/2117)
* Warning bar for MAU limit hit
[\#2114](https://github.com/matrix-org/matrix-react-sdk/pull/2114)
* Recognise server notices room(s)
[\#2112](https://github.com/matrix-org/matrix-react-sdk/pull/2112)
* Update room tags behaviour to match spec more
[\#2111](https://github.com/matrix-org/matrix-react-sdk/pull/2111)
* while logging out ignore `Session.logged_out` as it is intentional
[\#2058](https://github.com/matrix-org/matrix-react-sdk/pull/2058)
* Don't show 'connection lost' bar on MAU error
[\#2110](https://github.com/matrix-org/matrix-react-sdk/pull/2110)
* Support MAU error on sync
[\#2108](https://github.com/matrix-org/matrix-react-sdk/pull/2108)
* Support active user limit on message send
[\#2106](https://github.com/matrix-org/matrix-react-sdk/pull/2106)
* Run end to end tests as part of Travis build
[\#2091](https://github.com/matrix-org/matrix-react-sdk/pull/2091)
* Remove package-lock.json for now
[\#2097](https://github.com/matrix-org/matrix-react-sdk/pull/2097)
* Support montly active user limit error on /login
[\#2103](https://github.com/matrix-org/matrix-react-sdk/pull/2103)
* Unpin sanitize-html
[\#2105](https://github.com/matrix-org/matrix-react-sdk/pull/2105)
* Pin sanitize-html to 0.18.2
[\#2101](https://github.com/matrix-org/matrix-react-sdk/pull/2101)
* Make clicking on side panels close settings (mk 3)
[\#2096](https://github.com/matrix-org/matrix-react-sdk/pull/2096)
* Fix persistent element location not updating
[\#2092](https://github.com/matrix-org/matrix-react-sdk/pull/2092)
* fix Devtools input autofocus && state traversal when len === 1 && key=""
[\#2090](https://github.com/matrix-org/matrix-react-sdk/pull/2090)
* allow autocompleting Emoji by common aliases, e.g :+1: to :thumbsup:
[\#2085](https://github.com/matrix-org/matrix-react-sdk/pull/2085)
Changes in [0.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.0) (2018-07-30)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.0-rc.2...v0.13.0)
* Fix composer bug where cursor position would change when Riot regained focus
[\#2093](https://github.com/matrix-org/matrix-react-sdk/pull/2093)
* Fix persistend element location not updating
[\#2094](https://github.com/matrix-org/matrix-react-sdk/pull/2094)
* Slate Fixes 42?
[\#2089](https://github.com/matrix-org/matrix-react-sdk/pull/2089)
Changes in [0.13.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.0-rc.2) (2018-07-24)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.0-rc.1...v0.13.0-rc.2)
* Take jitsi conf calling out of labs
[\#2087](https://github.com/matrix-org/matrix-react-sdk/pull/2087)
Changes in [0.13.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.0-rc.1) (2018-07-24)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.9...v0.13.0-rc.1)
* Update from Weblate.
[\#2086](https://github.com/matrix-org/matrix-react-sdk/pull/2086)
* Moar Slate Fixes
[\#2082](https://github.com/matrix-org/matrix-react-sdk/pull/2082)
* Destroy the widget when its permission is revoked
[\#2081](https://github.com/matrix-org/matrix-react-sdk/pull/2081)
* Make ActiveWidgetStore clear persistent widgets
[\#2084](https://github.com/matrix-org/matrix-react-sdk/pull/2084)
* CreateRoomDialog is rendered before getting the config default_federate
[\#2078](https://github.com/matrix-org/matrix-react-sdk/pull/2078)
* Slate Fixes
[\#2076](https://github.com/matrix-org/matrix-react-sdk/pull/2076)
* FIX: Don't error on rooms the user has left already
[\#2077](https://github.com/matrix-org/matrix-react-sdk/pull/2077)
* Fix persistent apps being the wrong size
[\#2080](https://github.com/matrix-org/matrix-react-sdk/pull/2080)
* Fix widgets resetting when going to the top-left
[\#2079](https://github.com/matrix-org/matrix-react-sdk/pull/2079)
* Jitsi: Use integrations URL from config
[\#2062](https://github.com/matrix-org/matrix-react-sdk/pull/2062)
* Allow jitsi in e2e rooms
[\#2075](https://github.com/matrix-org/matrix-react-sdk/pull/2075)
* Fix border around persisted widgets
[\#2071](https://github.com/matrix-org/matrix-react-sdk/pull/2071)
* Fix e2e icons floating above jitsi
[\#2073](https://github.com/matrix-org/matrix-react-sdk/pull/2073)
* hide some commands after space as they have special semantics
[\#2074](https://github.com/matrix-org/matrix-react-sdk/pull/2074)
* Even More Slate Fixes :D
[\#2070](https://github.com/matrix-org/matrix-react-sdk/pull/2070)
* Improve UX for Jitsi by adding local echo for widgets
[\#2035](https://github.com/matrix-org/matrix-react-sdk/pull/2035)
* Jitsi: Check integrations server before call
[\#2063](https://github.com/matrix-org/matrix-react-sdk/pull/2063)
* Jitsi: Error message on no permission
[\#2061](https://github.com/matrix-org/matrix-react-sdk/pull/2061)
* Fix read receipts on top of Jitsi
[\#2065](https://github.com/matrix-org/matrix-react-sdk/pull/2065)
* Moar Slate Fixes
[\#2069](https://github.com/matrix-org/matrix-react-sdk/pull/2069)
* fix 2nd typo in one PR :(
[\#2068](https://github.com/matrix-org/matrix-react-sdk/pull/2068)
* check if has some completions, not if >=0
[\#2067](https://github.com/matrix-org/matrix-react-sdk/pull/2067)
* Slate fixes
[\#2066](https://github.com/matrix-org/matrix-react-sdk/pull/2066)
* Implement always-on-screen capability for widgets
[\#2056](https://github.com/matrix-org/matrix-react-sdk/pull/2056)
* simplify MessageComposerStore and improve its performance
[\#2064](https://github.com/matrix-org/matrix-react-sdk/pull/2064)
* Replace Draft with Slate
[\#1890](https://github.com/matrix-org/matrix-react-sdk/pull/1890)
* Fix not stopping to peek when navigating away from peeked room
[\#2055](https://github.com/matrix-org/matrix-react-sdk/pull/2055)
* T3chguy/slate cont2
[\#2049](https://github.com/matrix-org/matrix-react-sdk/pull/2049)
* add null-guard for stickerpickerWidget in StickerPicker
[\#2057](https://github.com/matrix-org/matrix-react-sdk/pull/2057)
* Implement always-on-screen capability for widgets
[\#2053](https://github.com/matrix-org/matrix-react-sdk/pull/2053)
* fix nullguard on EventTile, getComponent never returns falsey, it throws
[\#2024](https://github.com/matrix-org/matrix-react-sdk/pull/2024)
* Fix stickerpicker PersistedElement usage
[\#2051](https://github.com/matrix-org/matrix-react-sdk/pull/2051)
* encrypt for invited users if history visibility allows.
[\#2042](https://github.com/matrix-org/matrix-react-sdk/pull/2042)
* move nag bar clear statement to any desktop notif toggle not just 0->1
[\#2031](https://github.com/matrix-org/matrix-react-sdk/pull/2031)
* use TruncatedList to prevent rendering hundreds/thousands of DOM nodes
[\#2041](https://github.com/matrix-org/matrix-react-sdk/pull/2041)
* Fix stuff
[\#2047](https://github.com/matrix-org/matrix-react-sdk/pull/2047)
* Show m.room.server_acl
[\#2046](https://github.com/matrix-org/matrix-react-sdk/pull/2046)
Changes in [0.12.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.9) (2018-07-09)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.9-rc.2...v0.12.9)
* No changes since rc.1
Changes in [0.12.9-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.9-rc.2) (2018-07-06)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.9-rc.1...v0.12.9-rc.2)
* Implement aggregation by error type for tracked decryption failures
[\#2045](https://github.com/matrix-org/matrix-react-sdk/pull/2045)
* make new hiding of roomsublist behaviour opt-in
[\#2044](https://github.com/matrix-org/matrix-react-sdk/pull/2044)
* Implement aggregation by error type for tracked decryption failures
[\#2043](https://github.com/matrix-org/matrix-react-sdk/pull/2043)
* make new hiding of roomsublist behaviour opt-in
[\#2030](https://github.com/matrix-org/matrix-react-sdk/pull/2030)
Changes in [0.12.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.9-rc.1) (2018-07-04)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8...v0.12.9-rc.1)
* Update from Weblate.
[\#2040](https://github.com/matrix-org/matrix-react-sdk/pull/2040)
* Import react as React in src/components/views/messages/MStickerBody.js
[\#2039](https://github.com/matrix-org/matrix-react-sdk/pull/2039)
* Import react as React in src/GroupAddressPicker.js
[\#2038](https://github.com/matrix-org/matrix-react-sdk/pull/2038)
* Give PersistedElement a key
[\#2036](https://github.com/matrix-org/matrix-react-sdk/pull/2036)
* Revert " make click to insert nick work on join/parts, /me's etc"
[\#2034](https://github.com/matrix-org/matrix-react-sdk/pull/2034)
* Track an event name when tracking a decryption failure
[\#2033](https://github.com/matrix-org/matrix-react-sdk/pull/2033)
* warn on self-mute
[\#1974](https://github.com/matrix-org/matrix-react-sdk/pull/1974)
* make click to insert nick work on join/parts, /me's etc
[\#1945](https://github.com/matrix-org/matrix-react-sdk/pull/1945)
* Fix layout bug introduced by #2025
[\#2029](https://github.com/matrix-org/matrix-react-sdk/pull/2029)
* Fix room topics/names resetting when UserSetting re-renders
[\#2028](https://github.com/matrix-org/matrix-react-sdk/pull/2028)
* Improve tracking of UISIs
[\#2027](https://github.com/matrix-org/matrix-react-sdk/pull/2027)
* Replace share icons
[\#2026](https://github.com/matrix-org/matrix-react-sdk/pull/2026)
* Improve status bar errors (namely the consent error)
[\#2025](https://github.com/matrix-org/matrix-react-sdk/pull/2025)
* Fix incorrectly positioned copy button on `<pre>` blocks
[\#2023](https://github.com/matrix-org/matrix-react-sdk/pull/2023)
* Redact pathnames with origin `file://`
[\#2018](https://github.com/matrix-org/matrix-react-sdk/pull/2018)
* Update package-lock.json
[\#2022](https://github.com/matrix-org/matrix-react-sdk/pull/2022)
* on room sub list badge click goto first relevant room
[\#2021](https://github.com/matrix-org/matrix-react-sdk/pull/2021)
* improve linkifier AGAIN
[\#2020](https://github.com/matrix-org/matrix-react-sdk/pull/2020)
* fix historical section
[\#2016](https://github.com/matrix-org/matrix-react-sdk/pull/2016)
* Fix RoomSubList headers by re-commiting 1faecfd
[\#2014](https://github.com/matrix-org/matrix-react-sdk/pull/2014)
* don't fire share dialog when clicking timestamp of event,
[\#2017](https://github.com/matrix-org/matrix-react-sdk/pull/2017)
* Revert "affix copyButton so that it doesn't get scrolled horizontally"
[\#2013](https://github.com/matrix-org/matrix-react-sdk/pull/2013)
* when the user switches room, close room settings
[\#2019](https://github.com/matrix-org/matrix-react-sdk/pull/2019)
* Refactor widgets code
[\#2015](https://github.com/matrix-org/matrix-react-sdk/pull/2015)
* Login local errors for blank fields
[\#2009](https://github.com/matrix-org/matrix-react-sdk/pull/2009)
* Update lolex to 2.7.0
[\#1917](https://github.com/matrix-org/matrix-react-sdk/pull/1917)
* Improve Linkifier
[\#2011](https://github.com/matrix-org/matrix-react-sdk/pull/2011)
* use enum constants for EventStatus and correct isSent check
[\#2010](https://github.com/matrix-org/matrix-react-sdk/pull/2010)
* accent insensitive autocomplete
[\#2007](https://github.com/matrix-org/matrix-react-sdk/pull/2007)
* default to not showing url previews in e2ee rooms.
[\#2001](https://github.com/matrix-org/matrix-react-sdk/pull/2001)
* allow chaining right click contextmenus
[\#1999](https://github.com/matrix-org/matrix-react-sdk/pull/1999)
* hide empty roomsublists when filtering via search/tagpanel
[\#1954](https://github.com/matrix-org/matrix-react-sdk/pull/1954)
* prevent user,room,group autocomplete firing mid-word
[\#2012](https://github.com/matrix-org/matrix-react-sdk/pull/2012)
* fix instances of composer not getting/regaining focus
[\#2008](https://github.com/matrix-org/matrix-react-sdk/pull/2008)
* notif panel fixes
[\#2006](https://github.com/matrix-org/matrix-react-sdk/pull/2006)
* factor out conditional LanguageSelector as functional component
[\#2003](https://github.com/matrix-org/matrix-react-sdk/pull/2003)
* Autocomplete and Pillify Communities
[\#1993](https://github.com/matrix-org/matrix-react-sdk/pull/1993)
* Very basic Jitsi integration
[\#1971](https://github.com/matrix-org/matrix-react-sdk/pull/1971)
* add additional classes which protect the text from overflowing
[\#1994](https://github.com/matrix-org/matrix-react-sdk/pull/1994)
* Upload File confirmation modal steals focus, send it back to composer
[\#1992](https://github.com/matrix-org/matrix-react-sdk/pull/1992)
* delint MImageBody, fixes anonymous class and hyphenated style keys which
made react cry
[\#1991](https://github.com/matrix-org/matrix-react-sdk/pull/1991)
* allow using tab to navigate room list in a smarter way
[\#1977](https://github.com/matrix-org/matrix-react-sdk/pull/1977)
* fix no displayname usersettings
[\#1990](https://github.com/matrix-org/matrix-react-sdk/pull/1990)
* trigger TagTile context menu on right click
[\#1989](https://github.com/matrix-org/matrix-react-sdk/pull/1989)
* hide already chosen results from AddressPickerDialog
[\#2000](https://github.com/matrix-org/matrix-react-sdk/pull/2000)
* delint ChatCreateOrReuseDialog
[\#2002](https://github.com/matrix-org/matrix-react-sdk/pull/2002)
* fix set password & email flow possible to get stuck and onBlur murdering
your email
[\#1982](https://github.com/matrix-org/matrix-react-sdk/pull/1982)
Changes in [0.12.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8) (2018-06-29)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8-rc.2...v0.12.8)
* Revert "affix copyButton so that it doesn't get scrolled horizontally"
[\#2013](https://github.com/matrix-org/matrix-react-sdk/pull/2013)
* don't fire share dialog when clicking timestamp of event
[\#2017](https://github.com/matrix-org/matrix-react-sdk/pull/2017)
* when the user switches room, close room settings
[\#2019](https://github.com/matrix-org/matrix-react-sdk/pull/2019)
Changes in [0.12.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8-rc.2) (2018-06-22)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.8-rc.1...v0.12.8-rc.2)
* slash got consumed in the consolidation
[\#1998](https://github.com/matrix-org/matrix-react-sdk/pull/1998)
Changes in [0.12.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.8-rc.1) (2018-06-21)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7...v0.12.8-rc.1)
* Update from Weblate.
[\#1997](https://github.com/matrix-org/matrix-react-sdk/pull/1997)
* refactor, consolidate and improve SlashCommands
[\#1988](https://github.com/matrix-org/matrix-react-sdk/pull/1988)
* Take replies out of labs!
[\#1996](https://github.com/matrix-org/matrix-react-sdk/pull/1996)
* re-merge reset PR
[\#1987](https://github.com/matrix-org/matrix-react-sdk/pull/1987)
* once command has a space, strict match instead of fuzzy match
[\#1985](https://github.com/matrix-org/matrix-react-sdk/pull/1985)
* Fix matrix.to URL RegExp
[\#1986](https://github.com/matrix-org/matrix-react-sdk/pull/1986)
* Fix blank sticker picker
[\#1984](https://github.com/matrix-org/matrix-react-sdk/pull/1984)
* fix e2ee file/media stuff
[\#1972](https://github.com/matrix-org/matrix-react-sdk/pull/1972)
* right click for room tile context menu
[\#1978](https://github.com/matrix-org/matrix-react-sdk/pull/1978)
* only show m.room.message in FilePanel
[\#1983](https://github.com/matrix-org/matrix-react-sdk/pull/1983)
* improve command provider
[\#1981](https://github.com/matrix-org/matrix-react-sdk/pull/1981)
* affix copyButton so that it doesn't get scrolled horizontally
[\#1980](https://github.com/matrix-org/matrix-react-sdk/pull/1980)
* split continuation if there is a gap in conversation
[\#1979](https://github.com/matrix-org/matrix-react-sdk/pull/1979)
* fix a bunch of instances of react console spam
[\#1973](https://github.com/matrix-org/matrix-react-sdk/pull/1973)
* Track decryption success/failure rate with piwik
[\#1949](https://github.com/matrix-org/matrix-react-sdk/pull/1949)
* route matrix.to/#/+... links internally (not just group ids)
[\#1975](https://github.com/matrix-org/matrix-react-sdk/pull/1975)
* implement `hitting enter after Ctrl-K should switch to the first result`
[\#1976](https://github.com/matrix-org/matrix-react-sdk/pull/1976)
* Remove tag panel feature flag
[\#1970](https://github.com/matrix-org/matrix-react-sdk/pull/1970)
* QuestionDialog pass hasCancelButton to DialogButtons
[\#1968](https://github.com/matrix-org/matrix-react-sdk/pull/1968)
* check type before msgtype in the case of `m.sticker` with msgtype
[\#1965](https://github.com/matrix-org/matrix-react-sdk/pull/1965)
* apply roomlist searchFilter to aliases if it begins with a `#`
[\#1957](https://github.com/matrix-org/matrix-react-sdk/pull/1957)
* Share Dialog
[\#1948](https://github.com/matrix-org/matrix-react-sdk/pull/1948)
* make RoomTooltip generic and add ContextMenu&Tooltip to GroupInviteTile
[\#1950](https://github.com/matrix-org/matrix-react-sdk/pull/1950)
* Fix widgets re-appearing after being deleted
[\#1958](https://github.com/matrix-org/matrix-react-sdk/pull/1958)
* Fix crash on unspecified thumbnail info, and handle gracefully
[\#1967](https://github.com/matrix-org/matrix-react-sdk/pull/1967)
* fix styling of clearButton when its not there
[\#1964](https://github.com/matrix-org/matrix-react-sdk/pull/1964)
* Implement slightly magical CSS soln. to thumbnail sizing
[\#1912](https://github.com/matrix-org/matrix-react-sdk/pull/1912)
* Select audio output for WebRTC
[\#1932](https://github.com/matrix-org/matrix-react-sdk/pull/1932)
* move css rule to be more generic; remove overriden rule
[\#1962](https://github.com/matrix-org/matrix-react-sdk/pull/1962)
* improve tag panel accessibility and remove a no-op dispatch
[\#1960](https://github.com/matrix-org/matrix-react-sdk/pull/1960)
* Revert "Fix exception when opening dev tools"
[\#1963](https://github.com/matrix-org/matrix-react-sdk/pull/1963)
* fix message appears unencrypted while encrypting and not_sent
[\#1959](https://github.com/matrix-org/matrix-react-sdk/pull/1959)
* Fix exception when opening dev tools
[\#1961](https://github.com/matrix-org/matrix-react-sdk/pull/1961)
* show redacted stickers like other redacted messages
[\#1956](https://github.com/matrix-org/matrix-react-sdk/pull/1956)
* add mx_filterFlipColor to mx_MemberInfo_cancel img
[\#1951](https://github.com/matrix-org/matrix-react-sdk/pull/1951)
* don't set the displayname on registration as Synapse now does it
[\#1953](https://github.com/matrix-org/matrix-react-sdk/pull/1953)
* allow CreateRoom to scale properly horizontally
[\#1955](https://github.com/matrix-org/matrix-react-sdk/pull/1955)
* Keep context menus that extend downwards vertically on screen
[\#1952](https://github.com/matrix-org/matrix-react-sdk/pull/1952)
* re-run checkIfAlone if a member change occurred in the active room
[\#1947](https://github.com/matrix-org/matrix-react-sdk/pull/1947)
* Persist pinned message open-ness between room switches
[\#1935](https://github.com/matrix-org/matrix-react-sdk/pull/1935)
* Pinned message cosmetic improvements
[\#1933](https://github.com/matrix-org/matrix-react-sdk/pull/1933)
* Update sinon to 5.0.7
[\#1916](https://github.com/matrix-org/matrix-react-sdk/pull/1916)
* re-run checkIfAlone if a member change occurred in the active room
[\#1946](https://github.com/matrix-org/matrix-react-sdk/pull/1946)
* Replace "Login as guest" with "Try the app first" on login page
[\#1937](https://github.com/matrix-org/matrix-react-sdk/pull/1937)
* kill stream when using gUM for permission to device labels to turn off
camera
[\#1931](https://github.com/matrix-org/matrix-react-sdk/pull/1931)
Changes in [0.12.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.7) (2018-06-12)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.7-rc.1...v0.12.7)
* No changes since rc.1
Changes in [0.12.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.7-rc.1) (2018-06-06)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.6...v0.12.7-rc.1)
* Update from Weblate.
[\#1944](https://github.com/matrix-org/matrix-react-sdk/pull/1944)
* Import react as React in src/components/views/elements/DNDTagTile.js
[\#1943](https://github.com/matrix-org/matrix-react-sdk/pull/1943)
* Fix click on faded left/right/middle panel -> close settings
[\#1940](https://github.com/matrix-org/matrix-react-sdk/pull/1940)
* Add null-guard to support browsers that don't support performance
[\#1942](https://github.com/matrix-org/matrix-react-sdk/pull/1942)
* Support third party integration managers in AppPermission
[\#1455](https://github.com/matrix-org/matrix-react-sdk/pull/1455)
* Update pinned messages in real time
[\#1934](https://github.com/matrix-org/matrix-react-sdk/pull/1934)
* Expose at-room power level setting
[\#1938](https://github.com/matrix-org/matrix-react-sdk/pull/1938)
Changes in [0.12.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.6) (2018-05-25)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.6-rc.1...v0.12.6)
@ -340,7 +913,7 @@ Changes in [0.12.0-rc.7](https://github.com/matrix-org/matrix-react-sdk/releases
[\#1816](https://github.com/matrix-org/matrix-react-sdk/pull/1816)
* Improve group joining/leaving feedback
[\#1831](https://github.com/matrix-org/matrix-react-sdk/pull/1831)
Changes in [0.12.0-rc.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.6) (2018-04-09)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.5...v0.12.0-rc.6)

View file

@ -11,16 +11,8 @@ a 'skin'. A skin provides:
* The containing application
* Zero or more 'modules' containing non-UI functionality
**WARNING: As of July 2016, the skinning abstraction is broken due to rapid
development of `matrix-react-sdk` to meet the needs of Riot (codenamed Vector), the first app
to be built on top of the SDK** (https://github.com/vector-im/riot-web).
Right now `matrix-react-sdk` depends on some functionality from `riot-web`
(e.g. CSS), and `matrix-react-sdk` contains some Riot specific behaviour
(grep for 'vector'). This layering will be fixed asap once Riot development
has stabilised, but for now we do not advise trying to create new skins for
matrix-react-sdk until the layers are clearly separated again.
In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should
As of Aug 2018, the only skin that exists is `vector-im/riot-web`; it and
`matrix-org/matrix-react-sdk` should effectively
be considered as a single project (for instance, matrix-react-sdk bugs
are currently filed against vector-im/riot-web rather than this project).
@ -48,15 +40,14 @@ https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
Please follow the Matrix JS/React code style as per:
https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md
Whilst the layering separation between matrix-react-sdk and Riot is broken
(as of July 2016), code should be committed as follows:
Code should be committed as follows:
* All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
* Riot-specific components: https://github.com/vector-im/riot-web/tree/master/src/components
* In practice, `matrix-react-sdk` is still evolving so fast that the maintenance
burden of customising and overriding these components for Riot can seriously
impede development. So right now, there should be very few (if any) customisations for Riot.
* CSS for Matrix SDK components: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk
* CSS for Riot-specific overrides and components: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/riot-web
* CSS: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk
* Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
React components in matrix-react-sdk are come in two different flavours:
'structures' and 'views'. Structures are stateful components which handle the
@ -84,6 +75,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold:
* Per-view CSS is optional - it could choose to inherit all its styling from
the context of the rest of the app, although this is unusual for any but
* Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
structural components (lacking presentation logic) and the simplest view
components.
@ -139,8 +131,7 @@ for now.
OUTDATED: To Create Your Own Skin
=================================
**This is ALL LIES currently, as skinning is currently broken - see the WARNING
section at the top of this readme.**
**This is ALL LIES currently, and needs to be updated**
Skins are modules are exported from such a package in the `lib` directory.
`lib/skins` contains one directory per-skin, named after the skin, and the

View file

@ -93,6 +93,16 @@ Simply call `SettingsStore.getDisplayName`. The appropriate display name will be
Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting definition and should go through the helper functions on `SettingsStore`.
Although features have levels and a default value, the calculation of those options is blocked by the feature's state. A feature's state is determined from the `SdkConfig` and is a little complex. If `enableLabs` (a legacy flag) is `true` then the feature's state is `labs`, if it is `false`, the state is `disable`. If `enableLabs` is not set then the state is determined from the `features` config, such as in the following:
```json
"features": {
"feature_lazyloading": "labs"
}
```
In this example, `feature_lazyloading` is in the `labs` state. It may also be in the `enable` or `disable` state with a similar approach. If the state is invalid, the feature is in the `disable` state. A feature's levels are only calculated if it is in the `labs` state, therefore the default only applies in that scenario. If the state is `enable`, the feature is always-on.
Once a feature flag has served its purpose, it is generally recommended to remove it and the associated feature flag checks. This would enable the feature implicitly as it is part of the application now.
### Determining if a feature is enabled
A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the required calculations to determine if the feature is enabled based upon the configuration and user selection.

88
docs/slate-formats.md Normal file
View file

@ -0,0 +1,88 @@
Guide to data types used by the Slate-based Rich Text Editor
------------------------------------------------------------
We always store the Slate editor state in its Value form.
The schema for the Value is the same whether the editor is in MD or rich text mode, and is currently (rather arbitrarily)
dictated by the schema expected by slate-md-serializer, simply because it was the only bit of the pipeline which
has opinions on the schema. (slate-html-serializer lets you define how to serialize whatever schema you like).
The BLOCK_TAGS and MARK_TAGS give the mapping from HTML tags to the schema's node types (for blocks, which describe
block content like divs, and marks, which describe inline formatted sections like spans).
We use <p/> as the parent tag for the message (XXX: although some tags are technically not allowed to be nested within p's)
Various conversions are performed as content is moved between HTML, MD, and plaintext representations of HTML and MD.
The primitives used are:
* Markdown.js - models commonmark-formatted MD strings (as entered by the composer in MD mode)
* toHtml() - renders them to HTML suitable for sending on the wire
* isPlainText() - checks whether the parsed MD contains anything other than simple text.
* toPlainText() - renders MD to plain text in order to remove backslashes. Only works if the MD is already plaintext (otherwise it just emits HTML)
* slate-html-serializer
* converts Values to HTML (serialising) using our schema rules
* converts HTML to Values (deserialising) using our schema rules
* slate-md-serializer
* converts rich Values to MD strings (serialising) but using a non-commonmark generic MD dialect.
* This should use commonmark, but we use the serializer here for expedience rather than writing a commonmark one.
* slate-plain-serializer
* converts Values to plain text strings (serialising them) by concatenating the strings together
* converts Values from plain text strings (deserialiasing them).
* Used to initialise the editor by deserializing "" into a Value. Apparently this is the idiomatic way to initialise a blank editor.
* Used (as a bodge) to turn a rich text editor into a MD editor, when deserialising the converted MD string of the editor into a value
* PlainWithPillsSerializer
* A fork of slate-plain-serializer which is aware of Pills (hence the name) and Emoji.
* It can be configured to output Pills as:
* "plain": Pills are rendered via their 'completion' text - e.g. 'Matthew'; used for sending messages)
* "md": Pills are rendered as MD, e.g. [Matthew](https://matrix.to/#/@matthew:matrix.org) )
* "id": Pills are rendered as IDs, e.g. '@matthew:matrix.org' (used for authoring / commands)
* Emoji nodes are converted to inline utf8 emoji.
The actual conversion transitions are:
* Quoting:
* The message being quoted is taken as HTML
* ...and deserialised into a Value
* ...and then serialised into MD via slate-md-serializer if the editor is in MD mode
* Roundtripping between MD and rich text editor mode
* From MD to richtext (mdToRichEditorState):
* Serialise the MD-format Value to a MD string (converting pills to MD) with PlainWithPillsSerializer in 'md' mode
* Convert that MD string to HTML via Markdown.js
* Deserialise that Value to HTML via slate-html-serializer
* From richtext to MD (richToMdEditorState):
* Serialise the richtext-format Value to a MD string with slate-md-serializer (XXX: this should use commonmark)
* Deserialise that to a plain text value via slate-plain-serializer
* Loading history in one format into an editor which is in the other format
* Uses the same functions as for roundtripping
* Scanning the editor for a slash command
* If the editor is a single line node starting with /, then serialize it to a string with PlainWithPillsSerializer in 'id' mode
So that pills get converted to IDs suitable for commands being passed around
* Sending messages
* In RT mode:
* If there is rich content, serialize the RT-format Value to HTML body via slate-html-serializer
* Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode
* In MD mode:
* Serialize the MD-format Value into an MD string with PlainWithPillsSerializer in 'md' mode
* Parse the string with Markdown.js
* If it contains no formatting:
* Send as plaintext (as taken from Markdown.toPlainText())
* Otherwise
* Send as HTML (as taken from Markdown.toHtml())
* Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode
* Pasting HTML
* Deserialize HTML to a RT Value via slate-html-serializer
* In RT mode, insert it straight into the editor as a fragment
* In MD mode, serialise it to an MD string via slate-md-serializer and then insert the string into the editor as a fragment.
The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the above
gives sufficient detail on how it's all meant to work.

View file

@ -23,6 +23,7 @@ var fs = require('fs');
//
var testFile = process.env.KARMA_TEST_FILE || 'test/all-tests.js';
process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs';
function fileExists(name) {
@ -160,10 +161,9 @@ module.exports = function (config) {
webpack: {
module: {
loaders: [
{ test: /\.json$/, loader: "json" },
rules: [
{
test: /\.js$/, loader: "babel",
test: /\.js$/, loader: "babel-loader",
include: [path.resolve('./src'),
path.resolve('./test'),
]
@ -200,8 +200,9 @@ module.exports = function (config) {
'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
'sinon': 'sinon/pkg/sinon.js',
},
root: [
modules: [
path.resolve('./test'),
"node_modules"
],
},
devtool: 'inline-source-map',
@ -210,6 +211,8 @@ module.exports = function (config) {
// (the 'commonjs' here means it will output a 'require')
"electron": "commonjs electron",
},
// make sure we're flagged as development to avoid wasting time optimising
mode: 'development',
},
webpackMiddleware: {

6666
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.12.6",
"version": "0.13.6",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -38,10 +38,12 @@
"reskindex:watch": "node scripts/reskindex.js -h header -w",
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"build": "npm run reskindex && babel src -d lib --source-maps --copy-files",
"build:watch": "babel src -w -d lib --source-maps --copy-files",
"build": "npm run reskindex && npm run start:init",
"build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files",
"emoji-data-strip": "node scripts/emoji-data-strip.js",
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
"start": "npm run start:init && npm run start:all",
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"npm run build:watch\" \"npm run reskindex:watch\"",
"start:init": "babel src -d lib --source-maps --copy-files",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
"lintwithexclusions": "eslint --max-warnings 20 --ignore-path .eslintignore.errorfiles src test",
@ -51,7 +53,7 @@
"test-multi": "karma start"
},
"dependencies": {
"babel-runtime": "^6.11.6",
"babel-runtime": "^6.26.0",
"bluebird": "^3.5.0",
"blueimp-canvas-to-blob": "^3.5.0",
"browser-encrypt-attachment": "^0.3.0",
@ -59,9 +61,6 @@
"classnames": "^2.1.2",
"commonmark": "^0.28.1",
"counterpart": "^0.18.0",
"draft-js": "^0.11.0-alpha",
"draft-js-export-html": "^0.6.0",
"draft-js-export-markdown": "^0.3.0",
"emojione": "2.2.7",
"file-saver": "^1.3.3",
"filesize": "3.5.6",
@ -73,70 +72,76 @@
"glob": "^5.0.14",
"highlight.js": "^9.0.0",
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"linkifyjs": "^2.1.6",
"lodash": "^4.13.1",
"lolex": "2.3.2",
"matrix-js-sdk": "0.10.3",
"matrix-js-sdk": "0.11.1",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"prop-types": "^15.5.8",
"qrcode-react": "^0.1.16",
"querystring": "^0.2.0",
"react": "^15.6.0",
"react-addons-css-transition-group": "15.3.2",
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^15.6.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.14.1",
"resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4",
"slate": "^0.41.2",
"slate-html-serializer": "^0.6.1",
"slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3",
"slate-react": "^0.18.10",
"text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0",
"velocity-vector": "vector-im/velocity#059e3b2",
"whatwg-fetch": "^1.0.0"
"whatwg-fetch": "^1.1.1"
},
"devDependencies": {
"babel-cli": "^6.5.2",
"babel-core": "^6.14.0",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
"babel-loader": "^7.1.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.16.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-polyfill": "^6.5.0",
"babel-preset-es2015": "^6.14.0",
"babel-preset-es2016": "^6.11.3",
"babel-preset-es2017": "^6.14.0",
"babel-preset-react": "^6.11.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-es2016": "^6.24.1",
"babel-preset-es2017": "^6.24.1",
"babel-preset-react": "^6.24.1",
"chokidar": "^1.6.1",
"concurrently": "^4.0.1",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^7.7.0",
"estree-walker": "^0.5.0",
"expect": "^1.16.0",
"flow-parser": "^0.57.3",
"json-loader": "^0.5.3",
"karma": "^1.7.0",
"karma": "^3.0.0",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^0.1.2",
"karma-junit-reporter": "^0.4.1",
"karma-cli": "^1.0.1",
"karma-junit-reporter": "^0.4.2",
"karma-logcapture-reporter": "0.0.1",
"karma-mocha": "^0.2.2",
"karma-mocha": "^1.3.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "^0.0.31",
"karma-summary-reporter": "^1.3.3",
"karma-webpack": "^1.7.0",
"karma-summary-reporter": "^1.5.1",
"karma-webpack": "^4.0.0-beta.0",
"matrix-mock-request": "^1.2.1",
"matrix-react-test-utils": "^0.1.1",
"mocha": "^5.0.5",
"parallelshell": "^3.0.2",
"react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1",
"rimraf": "^2.4.3",
"sinon": "^1.17.3",
"sinon": "^5.0.7",
"source-map-loader": "^0.2.3",
"walk": "^2.3.9",
"webpack": "^1.12.14"
"webpack": "^4.20.2",
"webpack-cli": "^3.1.1"
}
}

View file

@ -291,6 +291,10 @@ textarea {
vertical-align: middle;
}
.mx_emojione_selected {
background-color: $accent-color;
}
::-moz-selection {
background-color: $accent-color;
color: $selection-fg-color;

View file

@ -39,9 +39,11 @@
@import "./views/dialogs/_EncryptedEventDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_QuestionDialog.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss";
@import "./views/dialogs/_SetEmailDialog.scss";
@import "./views/dialogs/_SetMxIdDialog.scss";
@import "./views/dialogs/_SetPasswordDialog.scss";
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/directory/_NetworkDropdown.scss";
@import "./views/elements/_AccessibleButton.scss";
@ -66,6 +68,7 @@
@import "./views/groups/_GroupUserSettings.scss";
@import "./views/login/_InteractiveAuthEntryComponents.scss";
@import "./views/login/_ServerConfig.scss";
@import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_MEmoteBody.scss";
@import "./views/messages/_MFileBody.scss";
@ -98,6 +101,7 @@
@import "./views/rooms/_RoomSettings.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTooltip.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SearchableEntityList.scss";
@import "./views/rooms/_Stickers.scss";

View file

@ -16,7 +16,7 @@ limitations under the License.
.mx_ContextualMenu_wrapper {
position: fixed;
z-index: 2000;
z-index: 5000;
}
.mx_ContextualMenu_background {
@ -26,7 +26,7 @@ limitations under the License.
width: 100%;
height: 100%;
opacity: 1.0;
z-index: 2000;
z-index: 5000;
}
.mx_ContextualMenu {
@ -37,7 +37,7 @@ limitations under the License.
position: absolute;
padding: 6px;
font-size: 14px;
z-index: 2001;
z-index: 5001;
}
.mx_ContextualMenu.mx_ContextualMenu_right {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -54,6 +55,10 @@ limitations under the License.
}
.mx_LeftPanel .mx_AppTile_mini {
height: 132px;
}
.mx_LeftPanel .mx_RoomList_scrollbar {
order: 1;

View file

@ -56,6 +56,18 @@ limitations under the License.
flex: 1;
}
.mx_MatrixChat_syncError {
color: $accent-fg-color;
background-color: $warning-bg-color;
border-radius: 5px;
display: table;
padding: 30px;
position: absolute;
top: 100px;
left: 50%;
transform: translateX(-50%);
}
.mx_MatrixChat .mx_LeftPanel {
order: 1;

View file

@ -113,6 +113,8 @@ limitations under the License.
}
.mx_RoomStatusBar_connectionLostBar {
display: flex;
margin-top: 19px;
min-height: 58px;
}
@ -132,6 +134,7 @@ limitations under the License.
color: $primary-fg-color;
font-size: 13px;
opacity: 0.5;
padding-bottom: 20px;
}
.mx_RoomStatusBar_resend_link {

View file

@ -91,6 +91,10 @@ limitations under the License.
background-color: $accent-color;
}
.mx_RoomSubList_label .mx_RoomSubList_badge:hover {
filter: brightness($focus-brightness);
}
/*
.collapsed .mx_RoomSubList_badge {
display: none;

View file

@ -176,10 +176,7 @@ hr.mx_RoomView_myReadMarker {
z-index: 1000;
overflow: hidden;
-webkit-transition: all .2s ease-out;
-moz-transition: all .2s ease-out;
-ms-transition: all .2s ease-out;
-o-transition: all .2s ease-out;
transition: all .2s ease-out;
}
.mx_RoomView_statusArea_expanded {

View file

@ -17,7 +17,6 @@ limitations under the License.
.mx_TagPanel {
flex: 0 0 60px;
background-color: $tertiary-accent-color;
cursor: pointer;
display: flex;
flex-direction: column;
@ -25,7 +24,11 @@ limitations under the License.
justify-content: space-between;
}
.mx_TagPanel .mx_TagPanel_clearButton {
.mx_TagPanel_items_selected {
cursor: pointer;
}
.mx_TagPanel .mx_TagPanel_clearButton_container {
/* Constant height within flex mx_TagPanel */
height: 70px;
width: 60px;

View file

@ -23,6 +23,10 @@ limitations under the License.
padding-bottom: 12px;
}
.mx_CreateRoomDialog_input_container {
padding-right: 20px;
}
.mx_CreateRoomDialog_input {
font-size: 15px;
border-radius: 3px;
@ -30,4 +34,5 @@ limitations under the License.
padding: 9px;
color: $primary-fg-color;
background-color: $primary-bg-color;
width: 100%;
}

View file

@ -0,0 +1,19 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomUpgradeDialog {
padding-right: 70px;
}

View file

@ -0,0 +1,89 @@
/*
Copyright 2018 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ShareDialog {
// this is to center the content
padding-right: 58px;
}
.mx_ShareDialog hr {
margin-top: 25px;
margin-bottom: 25px;
border-color: $light-fg-color;
}
.mx_ShareDialog_content {
margin: 10px 0;
}
.mx_ShareDialog_matrixto {
display: flex;
justify-content: space-between;
border-radius: 5px;
border: solid 1px $light-fg-color;
margin-bottom: 10px;
margin-top: 30px;
padding: 10px;
}
.mx_ShareDialog_matrixto a {
text-decoration: none;
}
.mx_ShareDialog_matrixto_link {
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_ShareDialog_matrixto_copy {
flex-shrink: 0;
cursor: pointer;
margin-left: 20px;
display: inherit;
}
.mx_ShareDialog_matrixto_copy > div {
background-image: url($copy-button-url);
margin-left: 5px;
width: 20px;
height: 20px;
}
.mx_ShareDialog_split {
display: flex;
flex-wrap: wrap;
}
.mx_ShareDialog_qrcode_container {
float: left;
background-color: #ffffff;
padding: 5px; // makes qr code more readable in dark theme
border-radius: 5px;
height: 256px;
width: 256px;
margin-right: 64px;
}
.mx_ShareDialog_social_container {
display: inline-block;
width: 299px;
}
.mx_ShareDialog_social_icon {
display: inline-grid;
margin-right: 10px;
margin-bottom: 10px;
}

View file

@ -4,6 +4,7 @@
.mx_UserPill,
.mx_RoomPill,
.mx_GroupPill,
.mx_AtRoomPill {
border-radius: 16px;
display: inline-block;
@ -13,7 +14,8 @@
}
.mx_EventTile_body .mx_UserPill,
.mx_EventTile_body .mx_RoomPill {
.mx_EventTile_body .mx_RoomPill,
.mx_EventTile_body .mx_GroupPill {
cursor: pointer;
}
@ -25,6 +27,10 @@
padding-right: 5px;
}
.mx_UserPill_selected {
background-color: $accent-color ! important;
}
.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
.mx_EventTile_content .mx_AtRoomPill,
.mx_MessageComposer_input .mx_AtRoomPill {
@ -35,14 +41,25 @@
/* More specific to override `.markdown-body a` color */
.mx_EventTile_content .markdown-body a.mx_RoomPill,
.mx_RoomPill {
.mx_EventTile_content .markdown-body a.mx_GroupPill,
.mx_RoomPill,
.mx_GroupPill {
color: $accent-fg-color;
background-color: $rte-room-pill-color;
padding-right: 5px;
}
/* More specific to override `.markdown-body a` color */
.mx_EventTile_content .markdown-body a.mx_GroupPill,
.mx_GroupPill {
color: $accent-fg-color;
background-color: $rte-group-pill-color;
padding-right: 5px;
}
.mx_UserPill .mx_BaseAvatar,
.mx_RoomPill .mx_BaseAvatar,
.mx_GroupPill .mx_BaseAvatar,
.mx_AtRoomPill .mx_BaseAvatar {
position: relative;
left: -3px;

View file

@ -28,6 +28,18 @@ limitations under the License.
margin-top: -2px;
}
.mx_MatrixToolbar_info {
padding-left: 16px;
padding-right: 8px;
background-color: $info-bg-color;
}
.mx_MatrixToolbar_error {
padding-left: 16px;
padding-right: 8px;
background-color: $warning-bg-color;
}
.mx_MatrixToolbar_content {
flex: 1;
}
@ -59,4 +71,4 @@ limitations under the License.
.mx_MatrixToolbar_changelog {
white-space: pre;
}
}

View file

@ -0,0 +1,37 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CreateEvent {
background-color: $info-plinth-bg-color;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.mx_CreateEvent_image {
float: left;
padding-right: 20px;
width: 72px;
height: 34px;
}
.mx_CreateEvent_header {
font-weight: bold;
}
.mx_CreateEvent_link {
}

View file

@ -20,5 +20,29 @@ limitations under the License.
}
.mx_MImageBody_thumbnail {
max-width: 100%;
}
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
.mx_MImageBody_thumbnail_container {
// Prevent the padding-bottom (added inline in MImageBody.js) from
// affecting elements below the container.
overflow: hidden;
// Make sure the _thumbnail is positioned relative to the _container
position: relative;
}
.mx_MImageBody_thumbnail_spinner {
position: absolute;
left: 50%;
top: 50%;
}
// Inner img and TintableSvg should be centered around 0, 0
.mx_MImageBody_thumbnail_spinner > * {
transform: translate(-50%, -50%);
}

View file

@ -14,33 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MStickerBody {
display: block;
margin-right: 34px;
min-height: 110px;
padding: 20px 0;
.mx_MStickerBody_wrapper {
padding: 20px 0px;
}
.mx_MStickerBody_image_container {
display: inline-block;
position: relative;
}
.mx_MStickerBody_image {
max-width: 100%;
opacity: 0;
}
.mx_MStickerBody_image_visible {
opacity: 1;
}
.mx_MStickerBody_placeholder {
position: absolute;
opacity: 1;
}
.mx_MStickerBody_placeholder_invisible {
transition: 500ms;
opacity: 0;
.mx_MStickerBody_tooltip {
position: absolute;
top: 50%;
}

View file

@ -17,8 +17,3 @@ limitations under the License.
.mx_MTextBody {
white-space: pre-wrap;
}
.mx_MTextBody pre{
overflow-y: auto;
max-height: 30vh;
}

View file

@ -75,6 +75,22 @@ limitations under the License.
border-radius: 2px;
}
.mx_AppTile_mini {
max-width: 960px;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.mx_AppTile_persistedWrapper {
height: 280px;
}
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
height: 114px;
}
.mx_AppTileMenuBar {
margin: 0;
padding: 2px 10px;
@ -126,6 +142,18 @@ limitations under the License.
overflow: hidden;
}
.mx_AppTileBody_mini {
height: 112px;
width: 100%;
overflow: hidden;
}
.mx_AppTileBody_mini iframe {
border: none;
width: 100%;
height: 100%;
}
.mx_AppTileBody iframe {
width: 100%;
height: 280px;

View file

@ -69,7 +69,8 @@
flex-flow: wrap;
}
.mx_Autocomplete_Completion.selected {
.mx_Autocomplete_Completion.selected,
.mx_Autocomplete_Completion:hover {
background: $menu-bg-color;
outline: none;
}

View file

@ -31,7 +31,6 @@ limitations under the License.
top: 14px;
left: 8px;
cursor: pointer;
z-index: 2;
}
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
@ -187,7 +186,6 @@ limitations under the License.
.mx_EventTile_msgOption {
float: right;
text-align: right;
z-index: 1;
position: relative;
width: 90px;
@ -290,7 +288,6 @@ limitations under the License.
position: absolute;
top: 9px;
left: 46px;
z-index: 2;
cursor: pointer;
}
@ -391,6 +388,7 @@ limitations under the License.
.mx_EventTile_content .markdown-body pre {
overflow-x: overlay;
overflow-y: visible;
max-height: 30vh;
}
.mx_EventTile_content .markdown-body code {
@ -399,6 +397,12 @@ limitations under the License.
color: #333;
}
.mx_EventTile_pre_container {
// For correct positioning of _copyButton (See TextualBody)
position: relative;
}
// Inserted adjacent to <pre> blocks, (See TextualBody)
.mx_EventTile_copyButton {
position: absolute;
display: inline-block;
@ -412,7 +416,6 @@ limitations under the License.
}
.mx_EventTile_body pre {
position: relative;
border: 1px solid transparent;
}
@ -421,7 +424,7 @@ limitations under the License.
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
}
.mx_EventTile_body pre:hover .mx_EventTile_copyButton
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton
{
visibility: visible;
}
@ -443,6 +446,7 @@ limitations under the License.
.mx_EventTile_content .markdown-body h2
{
font-size: 1.5em;
border-bottom: none ! important; // override GFM
}
.mx_EventTile_content .markdown-body a {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +23,29 @@ limitations under the License.
position: relative;
}
.mx_MessageComposer_replaced_wrapper {
margin-left: auto;
margin-right: auto;
}
.mx_MessageComposer_replaced_valign {
height: 60px;
display: table-cell;
vertical-align: middle;
}
.mx_MessageComposer_roomReplaced_icon {
float: left;
margin-right: 20px;
margin-top: 5px;
width: 31px;
height: 31px;
}
.mx_MessageComposer_roomReplaced_header {
font-weight: bold;
}
.mx_MessageComposer_autocomplete_wrapper {
position: relative;
height: 0;
@ -70,6 +94,7 @@ limitations under the License.
flex: 1;
display: flex;
flex-direction: column;
cursor: text;
}
.mx_MessageComposer_input {
@ -78,12 +103,29 @@ limitations under the License.
display: flex;
flex-direction: column;
min-height: 60px;
justify-content: center;
justify-content: start;
align-items: flex-start;
font-size: 14px;
margin-right: 6px;
}
.mx_MessageComposer_editor {
width: 100%;
max-height: 120px;
min-height: 19px;
overflow: auto;
word-break: break-word;
}
// FIXME: rather unpleasant hack to get rid of <p/> margins.
// really we should be mixing in markdown-body from gfm.css instead
.mx_MessageComposer_editor > :first-child {
margin-top: 0 ! important;
}
.mx_MessageComposer_editor > :last-child {
margin-bottom: 0 ! important;
}
@keyframes visualbell
{
from { background-color: #faa }
@ -94,28 +136,6 @@ limitations under the License.
animation: 0.2s visualbell;
}
.mx_MessageComposer_input_empty .public-DraftEditorPlaceholder-root {
display: none;
}
.mx_MessageComposer_input .DraftEditor-root {
width: 100%;
flex: 1;
word-break: break-word;
max-height: 120px;
min-height: 21px;
overflow: auto;
}
.mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer {
/* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */
padding-top: 2px;
}
.mx_MessageComposer .public-DraftStyleDefault-block {
overflow-x: hidden;
}
.mx_MessageComposer_input blockquote {
color: $blockquote-fg-color;
margin: 0 0 16px;
@ -123,7 +143,7 @@ limitations under the License.
border-left: 4px solid $blockquote-bar-color;
}
.mx_MessageComposer_input pre.public-DraftStyleDefault-pre pre {
.mx_MessageComposer_input pre {
background-color: $rte-code-bg-color;
border-radius: 3px;
padding: 10px;

View file

@ -25,26 +25,29 @@ limitations under the License.
background-color: $event-selected-color;
}
.mx_PinnedEventTile .mx_PinnedEventTile_sender {
.mx_PinnedEventTile .mx_PinnedEventTile_sender,
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
color: #868686;
font-size: 0.8em;
vertical-align: top;
display: block;
display: inline-block;
padding-bottom: 3px;
}
.mx_PinnedEventTile .mx_EventTile_content {
margin-left: 50px;
position: relative;
top: 0;
left: 0;
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
padding-left: 15px;
display: none;
}
.mx_PinnedEventTile .mx_BaseAvatar {
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
float: left;
margin-right: 10px;
}
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
display: inline-block;
}
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
display: block;
}
@ -63,5 +66,12 @@ limitations under the License.
.mx_PinnedEventTile_gotoButton {
display: inline-block;
font-size: 0.8em;
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
}
.mx_PinnedEventTile_message {
margin-left: 50px;
position: relative;
top: 0;
left: 0;
}

View file

@ -20,6 +20,7 @@ limitations under the License.
margin-bottom: 20px;
}
.mx_RoomSettings_upgradeButton,
.mx_RoomSettings_leaveButton,
.mx_RoomSettings_unbanButton {
@mixin mx_DialogButton;
@ -27,11 +28,16 @@ limitations under the License.
margin-right: 8px;
}
.mx_RoomSettings_upgradeButton,
.mx_RoomSettings_leaveButton:hover,
.mx_RoomSettings_unbanButton:hover {
@mixin mx_DialogButton_hover;
}
.mx_RoomSettings_upgradeButton.danger {
@mixin mx_DialogButton_danger;
}
.mx_RoomSettings_integrationsButton_error {
position: relative;
cursor: not-allowed;

View file

@ -0,0 +1,48 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomUpgradeWarningBar {
text-align: center;
height: 176px;
background-color: $event-selected-color;
align-items: center;
flex-direction: column;
justify-content: center;
display: flex;
background-color: $preview-bar-bg-color;
-webkit-align-items: center;
padding-left: 20px;
padding-right: 20px;
}
.mx_RoomUpgradeWarningBar_header {
color: $warning-color;
font-weight: bold;
}
.mx_RoomUpgradeWarningBar_body {
color: $warning-color;
}
.mx_RoomUpgradeWarningBar_upgradelink {
color: $warning-color;
text-decoration: underline;
}
.mx_RoomUpgradeWarningBar_small {
color: $greyed-fg-color;
font-size: 70%;
}

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10px" height="12px" viewBox="0 0 10 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>48BF5D32-306C-4B20-88EB-24B1F743CAC9</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Typing-Indicator" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.4">
<g id="typing-indicator" transform="translate(-301.000000, -172.000000)" fill="#76CFA6">
<path d="M309.666667,175.666667 C309.666667,173.633333 308.033333,172 306,172 C303.966667,172 302.333333,173.633333 302.333333,175.666667 L302.333333,176.666667 L301,176.666667 L301,184 L306,184 L311,184 L311,176.666667 L309.666667,176.666667 L309.666667,175.666667 Z M306,176.666667 L303.666667,176.666667 L303.666667,175.666667 C303.666667,174.366667 304.7,173.333333 306,173.333333 C307.3,173.333333 308.333333,174.366667 308.333333,175.666667 L308.333333,176.666667 L306,176.666667 L306,176.666667 Z" id="verified_icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

12
res/img/e2e-not_sent.svg Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10px" height="12px" viewBox="0 0 10 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>48BF5D32-306C-4B20-88EB-24B1F743CAC9</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Typing-Indicator" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="typing-indicator" transform="translate(-301.000000, -172.000000)" fill="#f44">
<path d="M309.666667,175.666667 C309.666667,173.633333 308.033333,172 306,172 C303.966667,172 302.333333,173.633333 302.333333,175.666667 L302.333333,176.666667 L301,176.666667 L301,184 L306,184 L311,184 L311,176.666667 L309.666667,176.666667 L309.666667,175.666667 Z M306,176.666667 L303.666667,176.666667 L303.666667,175.666667 C303.666667,174.366667 304.7,173.333333 306,173.333333 C307.3,173.333333 308.333333,174.366667 308.333333,175.666667 L308.333333,176.666667 L306,176.666667 L306,176.666667 Z" id="verified_icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

6
res/img/icons-share.svg Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 481.6 481.6" style="enable-background:new 0 0 481.6 481.6;" xml:space="preserve" width="16px" height="16px">
<g>
<path stroke="#76CFA6" stroke-width="5" d="M381.6,309.4c-27.7,0-52.4,13.2-68.2,33.6l-132.3-73.9c3.1-8.9,4.8-18.5,4.8-28.4c0-10-1.7-19.5-4.9-28.5l132.2-73.8 c15.7,20.5,40.5,33.8,68.3,33.8c47.4,0,86.1-38.6,86.1-86.1S429,0,381.5,0s-86.1,38.6-86.1,86.1c0,10,1.7,19.6,4.9,28.5 l-132.1,73.8c-15.7-20.6-40.5-33.8-68.3-33.8c-47.4,0-86.1,38.6-86.1,86.1s38.7,86.1,86.2,86.1c27.8,0,52.6-13.3,68.4-33.9 l132.2,73.9c-3.2,9-5,18.7-5,28.7c0,47.4,38.6,86.1,86.1,86.1s86.1-38.6,86.1-86.1S429.1,309.4,381.6,309.4z M381.6,27.1 c32.6,0,59.1,26.5,59.1,59.1s-26.5,59.1-59.1,59.1s-59.1-26.5-59.1-59.1S349.1,27.1,381.6,27.1z M100,299.8 c-32.6,0-59.1-26.5-59.1-59.1s26.5-59.1,59.1-59.1s59.1,26.5,59.1,59.1S132.5,299.8,100,299.8z M381.6,454.5 c-32.6,0-59.1-26.5-59.1-59.1c0-32.6,26.5-59.1,59.1-59.1s59.1,26.5,59.1,59.1C440.7,428,414.2,454.5,381.6,454.5z" fill="#76cfa6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

15
res/img/matrix-m.svg Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
<rect width="100%" height="100%" fill="#FFFFFF"/>
<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
<path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
v107.6h-50.9V169.2H166.3z"/>
<path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,6 @@
<svg width="72" height="34" viewBox="0 0 72 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7.26087V1H28.7889V7.26087M1 7.26087V33H28.7889V7.26087M1 7.26087H28.7889M4.16583 4.13043H16.8291" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
<path d="M43.2109 7.26087V1H70.9999V7.26087M43.2109 7.26087V33H70.9999V7.26087M43.2109 7.26087H70.9999M46.3768 4.13043H59.0401" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
<path d="M27.03 28.8262C34.2226 28.8262 36.0207 26.343 36.0207 25.1014V16.0996C36.0207 12.1264 43.6283 11.3401 47.432 11.4436" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 623 B

13
res/img/room_replaced.svg Normal file
View file

@ -0,0 +1,13 @@
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="31" height="31" fill="black" fill-opacity="0"/>
<circle cx="15.5" cy="15.5" r="15.5" fill="#A2A2A2"/>
<path d="M22.8553 15.5C22.8553 19.5622 19.5622 22.8553 15.5 22.8553C11.4378 22.8553 8.14474 19.5622 8.14474 15.5C8.14474 11.4378 11.4378 8.14474 15.5 8.14474C19.5622 8.14474 22.8553 11.4378 22.8553 15.5ZM15.5 24.25C20.3325 24.25 24.25 20.3325 24.25 15.5C24.25 10.6675 20.3325 6.75 15.5 6.75C10.6675 6.75 6.75 10.6675 6.75 15.5C6.75 20.3325 10.6675 24.25 15.5 24.25Z" fill="white" stroke="white" stroke-width="0.5"/>
<rect x="16.2666" y="30.5032" width="1.5" height="29.4046" transform="rotate(179.987 16.2666 30.5032)" fill="#A2A2A2"/>
<rect x="8.89404" y="28.8434" width="1.5" height="29.6593" transform="rotate(-149.607 8.89404 28.8434)" fill="#A2A2A2"/>
<rect x="2.87988" y="24.495" width="1.5" height="30.0747" transform="rotate(-121.597 2.87988 24.495)" fill="#A2A2A2"/>
<rect x="2.16284" y="23.3413" width="1.5" height="29.6434" transform="rotate(-116.581 2.16284 23.3413)" fill="#A2A2A2"/>
<rect x="1.55176" y="22.1343" width="1.5" height="29.5016" transform="rotate(-111.584 1.55176 22.1343)" fill="#A2A2A2"/>
<path d="M9.5 17L7.5 20L5.5 17L9.5 17Z" fill="white" stroke="white"/>
<path d="M21.5 15L23.5 12L25.5 15H21.5Z" fill="white" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
res/img/social/email-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
res/img/social/facebook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
res/img/social/linkedin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
res/img/social/reddit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -19,6 +19,8 @@ $focus-brightness: 200%;
// red warning colour
$warning-color: #ff0064;
$warning-bg-color: #DF2A8B;
$info-bg-color: #2A9EDF;
// groups
$info-plinth-bg-color: #454545;

View file

@ -25,6 +25,9 @@ $focus-brightness: 125%;
// red warning colour
$warning-color: #ff0064;
// background colour for warnings
$warning-bg-color: #DF2A8B;
$info-bg-color: #2A9EDF;
$mention-user-pill-bg-color: #ff0064;
$other-user-pill-bg-color: rgba(0, 0, 0, 0.1);
@ -97,6 +100,7 @@ $voip-accept-color: #80f480;
$rte-bg-color: #e9e9e9;
$rte-code-bg-color: rgba(0, 0, 0, 0.04);
$rte-room-pill-color: #aaa;
$rte-group-pill-color: #aaa;
// ********************
@ -167,6 +171,10 @@ $progressbar-color: #000;
outline: none;
}
@define-mixin mx_DialogButton_danger {
background-color: $warning-color;
}
@define-mixin mx_DialogButton_hover {
}

View file

@ -12,6 +12,9 @@ const output = Object.keys(EMOJI_DATA).map(
category: datum.category,
emoji_order: datum.emoji_order,
};
if (datum.aliases.length > 0) {
newDatum.aliases = datum.aliases;
}
if (datum.aliases_ascii.length > 0) {
newDatum.aliases_ascii = datum.aliases_ascii;
}

View file

@ -143,7 +143,7 @@ function getTranslationsJs(file) {
// Validate tag replacements
if (node.arguments.length > 2) {
const tagMap = node.arguments[2];
for (const prop of tagMap.properties) {
for (const prop of tagMap.properties || []) {
if (prop.key.type === 'Literal') {
const tag = prop.key.value;
// RegExp same as in src/languageHandler.js
@ -158,6 +158,7 @@ function getTranslationsJs(file) {
} catch (e) {
console.log();
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
console.error(e);
process.exit(1);
}
}

View file

@ -39,9 +39,17 @@ function getRedactedHash(hash) {
return hash.replace(hashRegex, "#/$1");
}
// Return the current origin and hash separated with a `/`. This does not include query parameters.
// Return the current origin, path and hash separated with a `/`. This does
// not include query parameters.
function getRedactedUrl() {
const { origin, pathname, hash } = window.location;
const { origin, hash } = window.location;
let { pathname } = window.location;
// Redact paths which could contain unexpected PII
if (origin.startsWith('file://')) {
pathname = "/<redacted>/";
}
return origin + pathname + getRedactedHash(hash);
}
@ -191,9 +199,9 @@ class Analytics {
this._paq.push(['trackPageView']);
}
trackEvent(category, action, name) {
trackEvent(category, action, name, value) {
if (this.disabled) return;
this._paq.push(['trackEvent', category, action, name]);
this._paq.push(['trackEvent', category, action, name, value]);
}
logout() {

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -59,7 +59,11 @@ import sdk from './index';
import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher';
import SdkConfig from './SdkConfig';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore';
import ScalarAuthClient from './ScalarAuthClient';
global.mxCalls = {
//room_id: MatrixCall
@ -123,7 +127,7 @@ function _setCallListeners(call) {
description: _t(
"There are unknown devices in this room: "+
"if you proceed without verifying them, it will be "+
"possible for someone to eavesdrop on your call."
"possible for someone to eavesdrop on your call.",
),
button: _t('Review Devices'),
onFinished: function(confirmed) {
@ -246,117 +250,77 @@ function _onAction(payload) {
switch (payload.action) {
case 'place_call':
if (module.exports.getAnyActiveCall()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
title: _t('Existing Call'),
description: _t('You are already in a call.'),
});
return; // don't allow >1 call to be placed.
}
{
if (module.exports.getAnyActiveCall()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
title: _t('Existing Call'),
description: _t('You are already in a call.'),
});
return; // don't allow >1 call to be placed.
}
// if the runtime env doesn't do VoIP, whine.
if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser.'),
});
return;
}
// if the runtime env doesn't do VoIP, whine.
if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser.'),
});
return;
}
var room = MatrixClientPeg.get().getRoom(payload.room_id);
if (!room) {
console.error("Room %s does not exist.", payload.room_id);
return;
}
const room = MatrixClientPeg.get().getRoom(payload.room_id);
if (!room) {
console.error("Room %s does not exist.", payload.room_id);
return;
}
var members = room.getJoinedMembers();
if (members.length <= 1) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
description: _t('You cannot place a call with yourself.'),
});
return;
} else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
placeCall(call);
} else { // > 2
dis.dispatch({
action: "place_conference_call",
room_id: payload.room_id,
type: payload.type,
remote_element: payload.remote_element,
local_element: payload.local_element,
});
const members = room.getJoinedMembers();
if (members.length <= 1) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
description: _t('You cannot place a call with yourself.'),
});
return;
} else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
placeCall(call);
} else { // > 2
dis.dispatch({
action: "place_conference_call",
room_id: payload.room_id,
type: payload.type,
remote_element: payload.remote_element,
local_element: payload.local_element,
});
}
}
break;
case 'place_conference_call':
console.log("Place conference call in %s", payload.room_id);
if (!ConferenceHandler) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
description: _t('Conference calls are not supported in this client'),
});
} else if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser.'),
});
} else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
// Conference calls are implemented by sending the media to central
// server which combines the audio from all the participants together
// into a single stream. This is incompatible with end-to-end encryption
// because a central server would be decrypting the audio for each
// participant.
// Therefore we disable conference calling in E2E rooms.
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
description: _t('Conference calls are not supported in encrypted rooms'),
});
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
title: _t('Warning!'),
description: _t('Conference calling is in development and may not be reliable.'),
onFinished: (confirm)=>{
if (confirm) {
ConferenceHandler.createNewMatrixCall(
MatrixClientPeg.get(), payload.room_id,
).done(function(call) {
placeCall(call);
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Conference call failed: " + err);
Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
title: _t('Failed to set up conference call'),
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
});
});
}
},
});
}
_startCallApp(payload.room_id, payload.type);
break;
case 'incoming_call':
if (module.exports.getAnyActiveCall()) {
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
// in future we could signal a "local busy" as a warning to the caller.
// see https://github.com/vector-im/vector-web/issues/1964
return;
}
{
if (module.exports.getAnyActiveCall()) {
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
// in future we could signal a "local busy" as a warning to the caller.
// see https://github.com/vector-im/vector-web/issues/1964
return;
}
// if the runtime env doesn't do VoIP, stop here.
if (!MatrixClientPeg.get().supportsVoip()) {
return;
}
// if the runtime env doesn't do VoIP, stop here.
if (!MatrixClientPeg.get().supportsVoip()) {
return;
}
var call = payload.call;
_setCallListeners(call);
_setCallState(call, call.roomId, "ringing");
const call = payload.call;
_setCallListeners(call);
_setCallState(call, call.roomId, "ringing");
}
break;
case 'hangup':
if (!calls[payload.room_id]) {
@ -378,6 +342,112 @@ function _onAction(payload) {
break;
}
}
async function _startCallApp(roomId, type) {
// check for a working intgrations manager. Technically we could put
// the state event in anyway, but the resulting widget would then not
// work for us. Better that the user knows before everyone else in the
// room sees it.
const scalarClient = new ScalarAuthClient();
let haveScalar = false;
try {
await scalarClient.connect();
haveScalar = scalarClient.hasCredentials();
} catch (e) {
// fall through
}
if (!haveScalar) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, {
title: _t('Could not connect to the integration server'),
description: _t('A conference call could not be started because the intgrations server is not available'),
});
return;
}
dis.dispatch({
action: 'appsDrawer',
show: true,
});
const room = MatrixClientPeg.get().getRoom(roomId);
const currentRoomWidgets = WidgetUtils.getRoomWidgets(room);
if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, 'jitsi')) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t('A call is currently being placed!'),
});
return;
}
const currentJitsiWidgets = currentRoomWidgets.filter((ev) => {
return ev.getContent().type === 'jitsi';
});
if (currentJitsiWidgets.length > 0) {
console.warn(
"Refusing to start conference call widget in " + roomId +
" a conference call widget is already present",
);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t('A call is already in progress!'),
});
return;
}
// This inherits its poor naming from the field of the same name that goes into
// the event. It's just a random string to make the Jitsi URLs unique.
const widgetSessionId = Math.random().toString(36).substring(2);
const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId;
// NB. we can't just encodeURICompoent all of these because the $ signs need to be there
// (but currently the only thing that needs encoding is the confId)
const queryString = [
'confId='+encodeURIComponent(confId),
'isAudioConf='+(type === 'voice' ? 'true' : 'false'),
'displayName=$matrix_display_name',
'avatarUrl=$matrix_avatar_url',
'email=$matrix_user_id',
].join('&');
let widgetUrl;
if (SdkConfig.get().integrations_jitsi_widget_url) {
// Try this config key. This probably isn't ideal as a way of discovering this
// URL, but this will at least allow the integration manager to not be hardcoded.
widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString;
} else {
widgetUrl = SdkConfig.get().integrations_rest_url + '/widgets/jitsi.html?' + queryString;
}
const widgetData = { widgetSessionId };
const widgetId = (
'jitsi_' +
MatrixClientPeg.get().credentials.userId +
'_' +
Date.now()
);
WidgetUtils.setRoomWidget(roomId, widgetId, 'jitsi', widgetUrl, 'Jitsi', widgetData).then(() => {
console.log('Jitsi widget added');
}).catch((e) => {
if (e.errcode === 'M_FORBIDDEN') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
title: _t('Permission Required'),
description: _t("You do not have permission to start a conference call in this room"),
});
}
console.error(e);
});
}
// FIXME: Nasty way of making sure we only register
// with the dispatcher once
if (!global.mxCallHandler) {
@ -412,6 +482,24 @@ const callHandler = {
return null;
},
/**
* The conference handler is a module that deals with implementation-specific
* multi-party calling implementations. Riot passes in its own which creates
* a one-to-one call with a freeswitch conference bridge. As of July 2018,
* the de-facto way of conference calling is a Jitsi widget, so this is
* deprecated. It reamins here for two reasons:
* 1. So Riot still supports joining existing freeswitch conference calls
* (but doesn't support creating them). After a transition period, we can
* remove support for joining them too.
* 2. To hide the one-to-one rooms that old-style conferencing creates. This
* is much harder to remove: probably either we make Riot leave & forget these
* rooms after we remove support for joining freeswitch conferences, or we
* accept that random rooms with cryptic users will suddently appear for
* anyone who's ever used conference calling, or we are stuck with this
* code forever.
*
* @param {object} confHandler The conference handler object
*/
setConferenceHandler: function(confHandler) {
ConferenceHandler = confHandler;
},

View file

@ -22,34 +22,44 @@ export default {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
const audioIn = [];
const videoIn = [];
const audiooutput = [];
const audioinput = [];
const videoinput = [];
if (devices.some((device) => !device.label)) return false;
devices.forEach((device) => {
switch (device.kind) {
case 'audioinput': audioIn.push(device); break;
case 'videoinput': videoIn.push(device); break;
case 'audiooutput': audiooutput.push(device); break;
case 'audioinput': audioinput.push(device); break;
case 'videoinput': videoinput.push(device); break;
}
});
// console.log("Loaded WebRTC Devices", mediaDevices);
return {
audioinput: audioIn,
videoinput: videoIn,
audiooutput,
audioinput,
videoinput,
};
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
},
loadDevices: function() {
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
Matrix.setMatrixCallAudioOutput(audioOutDeviceId);
Matrix.setMatrixCallAudioInput(audioDeviceId);
Matrix.setMatrixCallVideoInput(videoDeviceId);
},
setAudioOutput: function(deviceId) {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioOutput(deviceId);
},
setAudioInput: function(deviceId) {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioInput(deviceId);

View file

@ -15,46 +15,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ContentState, convertToRaw, convertFromRaw} from 'draft-js';
import * as RichText from './RichText';
import Markdown from './Markdown';
import { Value } from 'slate';
import _clamp from 'lodash/clamp';
type MessageFormat = 'html' | 'markdown';
type MessageFormat = 'rich' | 'markdown';
class HistoryItem {
// Keeping message for backwards-compatibility
message: string;
rawContentState: RawDraftContentState;
format: MessageFormat = 'html';
// We store history items in their native format to ensure history is accurate
// and then convert them if our RTE has subsequently changed format.
value: Value;
format: MessageFormat = 'rich';
constructor(contentState: ?ContentState, format: ?MessageFormat) {
this.rawContentState = contentState ? convertToRaw(contentState) : null;
constructor(value: ?Value, format: ?MessageFormat) {
this.value = value;
this.format = format;
}
toContentState(outputFormat: MessageFormat): ContentState {
const contentState = convertFromRaw(this.rawContentState);
if (outputFormat === 'markdown') {
if (this.format === 'html') {
return ContentState.createFromText(RichText.stateToMarkdown(contentState));
}
} else {
if (this.format === 'markdown') {
return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML());
}
}
// history item has format === outputFormat
return contentState;
static fromJSON(obj: Object): HistoryItem {
return new HistoryItem(
Value.fromJSON(obj.value),
obj.format,
);
}
toJSON(): Object {
return {
value: this.value.toJSON(),
format: this.format,
};
}
}
export default class ComposerHistoryManager {
history: Array<HistoryItem> = [];
prefix: string;
lastIndex: number = 0;
currentIndex: number = 0;
lastIndex: number = 0; // used for indexing the storage
currentIndex: number = 0; // used for indexing the loaded validated history Array
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
this.prefix = prefix + roomId;
@ -62,23 +60,28 @@ export default class ComposerHistoryManager {
// TODO: Performance issues?
let item;
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
this.history.push(
Object.assign(new HistoryItem(), JSON.parse(item)),
);
try {
this.history.push(
HistoryItem.fromJSON(JSON.parse(item)),
);
} catch (e) {
console.warn("Throwing away unserialisable history", e);
}
}
this.lastIndex = this.currentIndex;
// reset currentIndex to account for any unserialisable history
this.currentIndex = this.history.length;
}
save(contentState: ContentState, format: MessageFormat) {
const item = new HistoryItem(contentState, format);
save(value: Value, format: MessageFormat) {
const item = new HistoryItem(value, format);
this.history.push(item);
this.currentIndex = this.lastIndex + 1;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
this.currentIndex = this.history.length;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
}
getItem(offset: number, format: MessageFormat): ?ContentState {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
const item = this.history[this.currentIndex];
return item ? item.toContentState(format) : null;
getItem(offset: number): ?HistoryItem {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}
}

View file

@ -243,6 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
const blob = new Blob([encryptResult.data]);
return matrixClient.uploadContent(blob, {
progressHandler: progressHandler,
includeFilename: false,
}).then(function(url) {
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and

View file

@ -0,0 +1,202 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export class DecryptionFailure {
constructor(failedEventId, errorCode) {
this.failedEventId = failedEventId;
this.errorCode = errorCode;
this.ts = Date.now();
}
}
export class DecryptionFailureTracker {
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
// are accumulated in `failureCounts`.
failures = [];
// A histogram of the number of failures that will be tracked at the next tracking
// interval, split by failure error code.
failureCounts = {
// [errorCode]: 42
};
// Event IDs of failures that were tracked previously
trackedEventHashMap = {
// [eventId]: true
};
// Set to an interval ID when `start` is called
checkInterval = null;
trackInterval = null;
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
static TRACK_INTERVAL_MS = 60000;
// Call `checkFailures` every `CHECK_INTERVAL_MS`.
static CHECK_INTERVAL_MS = 5000;
// Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting
// the failure in `failureCounts`.
static GRACE_PERIOD_MS = 60000;
/**
* Create a new DecryptionFailureTracker.
*
* Call `eventDecrypted(event, err)` on this instance when an event is decrypted.
*
* Call `start()` to start the tracker, and `stop()` to stop tracking.
*
* @param {function} fn The tracking function, which will be called when failures
* are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`,
* where `count` is the number of failures and `errorCode` matches the `.code` of
* provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified.
* @param {function?} errorCodeMapFn The function used to map error codes to the
* trackedErrorCode. If not provided, the `.code` of errors will be used.
*/
constructor(fn, errorCodeMapFn) {
if (!fn || typeof fn !== 'function') {
throw new Error('DecryptionFailureTracker requires tracking function');
}
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
}
this._trackDecryptionFailure = fn;
this._mapErrorCode = errorCodeMapFn;
}
// loadTrackedEventHashMap() {
// this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {};
// }
// saveTrackedEventHashMap() {
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
// }
eventDecrypted(e, err) {
if (err) {
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
} else {
// Could be an event in the failures, remove it
this.removeDecryptionFailuresForEvent(e);
}
}
addDecryptionFailure(failure) {
this.failures.push(failure);
}
removeDecryptionFailuresForEvent(e) {
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
}
/**
* Start checking for and tracking failures.
*/
start() {
this.checkInterval = setInterval(
() => this.checkFailures(Date.now()),
DecryptionFailureTracker.CHECK_INTERVAL_MS,
);
this.trackInterval = setInterval(
() => this.trackFailures(),
DecryptionFailureTracker.TRACK_INTERVAL_MS,
);
}
/**
* Clear state and stop checking for and tracking failures.
*/
stop() {
clearInterval(this.checkInterval);
clearInterval(this.trackInterval);
this.failures = [];
this.failureCounts = {};
}
/**
* Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
* tracked. Only mark one failure per event ID.
* @param {number} nowTs the timestamp that represents the time now.
*/
checkFailures(nowTs) {
const failuresGivenGrace = [];
const failuresNotReady = [];
while (this.failures.length > 0) {
const f = this.failures.shift();
if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) {
failuresGivenGrace.push(f);
} else {
failuresNotReady.push(f);
}
}
this.failures = failuresNotReady;
// Only track one failure per event
const dedupedFailuresMap = failuresGivenGrace.reduce(
(map, failure) => {
if (!this.trackedEventHashMap[failure.failedEventId]) {
return map.set(failure.failedEventId, failure);
} else {
return map;
}
},
// Use a map to preseve key ordering
new Map(),
);
const trackedEventIds = [...dedupedFailuresMap.keys()];
this.trackedEventHashMap = trackedEventIds.reduce(
(result, eventId) => ({...result, [eventId]: true}),
this.trackedEventHashMap,
);
// Commented out for now for expediency, we need to consider unbound nature of storing
// this in localStorage
// this.saveTrackedEventHashMap();
const dedupedFailures = dedupedFailuresMap.values();
this._aggregateFailures(dedupedFailures);
}
_aggregateFailures(failures) {
for (const failure of failures) {
const errorCode = failure.errorCode;
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
}
}
/**
* If there are failures that should be tracked, call the given trackDecryptionFailure
* function with the number of failures that should be tracked.
*/
trackFailures() {
for (const errorCode of Object.keys(this.failureCounts)) {
if (this.failureCounts[errorCode] > 0) {
const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode;
this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode);
this.failureCounts[errorCode] = 0;
}
}
}
}

View file

@ -18,6 +18,7 @@ import URL from 'url';
import dis from './dispatcher';
import IntegrationManager from './IntegrationManager';
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
const WIDGET_API_VERSION = '0.0.1'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
@ -155,6 +156,14 @@ export default class FromWidgetPostMessageApi {
const integType = (data && data.integType) ? data.integType : null;
const integId = (data && data.integId) ? data.integId : null;
IntegrationManager.open(integType, integId);
} else if (action === 'set_always_on_screen') {
// This is a new message: there is no reason to support the deprecated widgetData here
const data = event.data.data;
const val = data.value;
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
}
} else {
console.warn('Widget postMessage event unhandled');
this.sendError(event, {message: 'The postMessage was unhandled'});

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import Modal from './Modal';
import sdk from './';
import MultiInviter from './utils/MultiInviter';

View file

@ -112,7 +112,6 @@ export function charactersToImageNode(alt, useSvg, ...unicode) {
/>;
}
export function processHtmlForSending(html: string): string {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = html;
@ -130,13 +129,6 @@ export function processHtmlForSending(html: string): string {
if (i !== contentDiv.children.length - 1) {
contentHTML += '<br />';
}
} else if (element.tagName.toLowerCase() === 'pre') {
// Replace "<br>\n" with "\n" within `<pre>` tags because the <br> is
// redundant. This is a workaround for a bug in draft-js-export-html:
// https://github.com/sstur/draft-js-export-html/issues/62
contentHTML += '<pre>' +
element.innerHTML.replace(/<br>\n/g, '\n').trim() +
'</pre>';
} else {
const temp = document.createElement('div');
temp.appendChild(element.cloneNode(true));
@ -176,6 +168,99 @@ export function isUrlPermitted(inputUrl) {
}
}
const transformTags = { // custom to matrix
// add blank targets to all hyperlinks except vector URLs
'a': function(tagName, attribs) {
if (attribs.href) {
attribs.target = '_blank'; // by default
let m;
// FIXME: horrible duplication with linkify-matrix
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
if (m) {
attribs.href = m[1];
delete attribs.target;
} else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) {
const entity = m[1];
switch (entity[0]) {
case '@':
attribs.href = '#/user/' + entity;
break;
case '+':
attribs.href = '#/group/' + entity;
break;
case '#':
case '!':
attribs.href = '#/room/' + entity;
break;
}
delete attribs.target;
}
}
}
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName, attribs };
},
'img': function(tagName, attribs) {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s.
if (!attribs.src || !attribs.src.startsWith('mxc://')) {
return { tagName, attribs: {}};
}
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
attribs.src,
attribs.width || 800,
attribs.height || 600,
);
return { tagName, attribs };
},
'code': function(tagName, attribs) {
if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s+/).filter(function(cl) {
return cl.startsWith('language-');
});
attribs.class = classes.join(' ');
}
return { tagName, attribs };
},
'*': function(tagName, attribs) {
// Delete any style previously assigned, style is an allowedTag for font and span
// because attributes are stripped after transforming
delete attribs.style;
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents
const customCSSMapper = {
'data-mx-color': 'color',
'data-mx-bg-color': 'background-color',
// $customAttributeKey: $cssAttributeKey
};
let style = "";
Object.keys(customCSSMapper).forEach((customAttributeKey) => {
const cssAttributeKey = customCSSMapper[customAttributeKey];
const customAttributeValue = attribs[customAttributeKey];
if (customAttributeValue &&
typeof customAttributeValue === 'string' &&
COLOR_REGEX.test(customAttributeValue)
) {
style += cssAttributeKey + ":" + customAttributeValue + ";";
delete attribs[customAttributeKey];
}
});
if (style) {
attribs.style = style;
}
return { tagName, attribs };
},
};
const sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
@ -199,95 +284,14 @@ const sanitizeHtmlParams = {
allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
transformTags,
};
transformTags: { // custom to matrix
// add blank targets to all hyperlinks except vector URLs
'a': function(tagName, attribs) {
if (attribs.href) {
attribs.target = '_blank'; // by default
let m;
// FIXME: horrible duplication with linkify-matrix
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
if (m) {
attribs.href = m[1];
delete attribs.target;
} else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) {
const entity = m[1];
if (entity[0] === '@') {
attribs.href = '#/user/' + entity;
} else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity;
}
delete attribs.target;
}
}
}
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs: attribs };
},
'img': function(tagName, attribs) {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s.
if (!attribs.src || !attribs.src.startsWith('mxc://')) {
return { tagName, attribs: {}};
}
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
attribs.src,
attribs.width || 800,
attribs.height || 600,
);
return { tagName: tagName, attribs: attribs };
},
'code': function(tagName, attribs) {
if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s+/).filter(function(cl) {
return cl.startsWith('language-');
});
attribs.class = classes.join(' ');
}
return {
tagName: tagName,
attribs: attribs,
};
},
'*': function(tagName, attribs) {
// Delete any style previously assigned, style is an allowedTag for font and span
// because attributes are stripped after transforming
delete attribs.style;
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents
const customCSSMapper = {
'data-mx-color': 'color',
'data-mx-bg-color': 'background-color',
// $customAttributeKey: $cssAttributeKey
};
let style = "";
Object.keys(customCSSMapper).forEach((customAttributeKey) => {
const cssAttributeKey = customCSSMapper[customAttributeKey];
const customAttributeValue = attribs[customAttributeKey];
if (customAttributeValue &&
typeof customAttributeValue === 'string' &&
COLOR_REGEX.test(customAttributeValue)
) {
style += cssAttributeKey + ":" + customAttributeValue + ";";
delete attribs[customAttributeKey];
}
});
if (style) {
attribs.style = style;
}
return { tagName: tagName, attribs: attribs };
},
},
// this is the same as the above except with less rewriting
const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams);
composerSanitizeHtmlParams.transformTags = {
'code': transformTags['code'],
'*': transformTags['*'],
};
class BaseHighlighter {
@ -402,21 +406,30 @@ class TextHighlighter extends BaseHighlighter {
}
/* turn a matrix event body into html
*
* content: 'content' of the MatrixEvent
*
* highlights: optional list of words to highlight, ordered by longest word first
*
* opts.highlightLink: optional href to add to highlighted words
* opts.disableBigEmoji: optional argument to disable the big emoji class.
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
*/
/* turn a matrix event body into html
*
* content: 'content' of the MatrixEvent
*
* highlights: optional list of words to highlight, ordered by longest word first
*
* opts.highlightLink: optional href to add to highlighted words
* opts.disableBigEmoji: optional argument to disable the big emoji class.
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
* opts.returnString: return an HTML string rather than JSX elements
* opts.emojiOne: optional param to do emojiOne (default true)
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
*/
export function bodyToHtml(content, highlights, opts={}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
let bodyHasEmoji = false;
let sanitizeParams = sanitizeHtmlParams;
if (opts.forComposerQuote) {
sanitizeParams = composerSanitizeHtmlParams;
}
let strippedBody;
let safeBody;
let isDisplayedWithHtml;
@ -428,10 +441,10 @@ export function bodyToHtml(content, highlights, opts={}) {
if (highlights && highlights.length > 0) {
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
const safeHighlights = highlights.map(function(highlight) {
return sanitizeHtml(highlight, sanitizeHtmlParams);
return sanitizeHtml(highlight, sanitizeParams);
});
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
sanitizeHtmlParams.textFilter = function(safeText) {
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
};
}
@ -440,19 +453,20 @@ export function bodyToHtml(content, highlights, opts={}) {
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
if (doEmojiOne) {
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
}
// Only generate safeBody if the message was sent as org.matrix.custom.html
if (isHtmlMessage) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(formattedBody, sanitizeHtmlParams);
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
} else {
// ... or if there are emoji, which we insert as HTML alongside the
// escaped plaintext body.
if (bodyHasEmoji) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(escape(strippedBody), sanitizeHtmlParams);
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
}
}
@ -463,7 +477,11 @@ export function bodyToHtml(content, highlights, opts={}) {
safeBody = unicodeToImage(safeBody);
}
} finally {
delete sanitizeHtmlParams.textFilter;
delete sanitizeParams.textFilter;
}
if (opts.returnString) {
return isDisplayedWithHtml ? safeBody : strippedBody;
}
let emojiBody = false;

View file

@ -30,6 +30,8 @@ import DMRoomMap from './utils/DMRoomMap';
import RtsClient from './RtsClient';
import Modal from './Modal';
import sdk from './index';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
import PlatformPeg from "./PlatformPeg";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -157,6 +159,40 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
});
}
export function handleInvalidStoreError(e) {
if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) {
return Promise.resolve().then(() => {
const lazyLoadEnabled = e.value;
if (lazyLoadEnabled) {
const LazyLoadingResyncDialog =
sdk.getComponent("views.dialogs.LazyLoadingResyncDialog");
return new Promise((resolve) => {
Modal.createDialog(LazyLoadingResyncDialog, {
onFinished: resolve,
});
});
} else {
// show warning about simultaneous use
// between LL/non-LL version on same host.
// as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app)
const LazyLoadingDisabledDialog =
sdk.getComponent("views.dialogs.LazyLoadingDisabledDialog");
return new Promise((resolve) => {
Modal.createDialog(LazyLoadingDisabledDialog, {
onFinished: resolve,
host: window.location.host,
});
});
}
}).then(() => {
return MatrixClientPeg.get().store.deleteAllData();
}).then(() => {
PlatformPeg.get().reload();
});
}
}
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
console.log(`Doing guest login on ${hsUrl}`);
@ -385,6 +421,8 @@ function _persistCredentialsToLocalStorage(credentials) {
console.log(`Session persisted for ${credentials.userId}`);
}
let _isLoggingOut = false;
/**
* Logs the current session out and transitions to the logged-out state
*/
@ -404,6 +442,7 @@ export function logout() {
return;
}
_isLoggingOut = true;
MatrixClientPeg.get().logout().then(onLoggedOut,
(err) => {
// Just throwing an error here is going to be very unhelpful
@ -419,6 +458,10 @@ export function logout() {
).done();
}
export function isLoggingOut() {
return _isLoggingOut;
}
/**
* Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in.
@ -436,6 +479,7 @@ async function startMatrixClient() {
UserActivity.start();
Presence.start();
DMRoomMap.makeShared().start();
ActiveWidgetStore.start();
await MatrixClientPeg.start();
@ -449,6 +493,7 @@ async function startMatrixClient() {
* storage. Used after a session has been logged out.
*/
export function onLoggedOut() {
_isLoggingOut = false;
stopMatrixClient();
_clearStorage().done();
dis.dispatch({action: 'on_logged_out'});
@ -488,6 +533,7 @@ export function stopMatrixClient() {
Notifier.stop();
UserActivity.stop();
Presence.stop();
ActiveWidgetStore.stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
const cli = MatrixClientPeg.get();
if (cli) {

View file

@ -102,6 +102,16 @@ export default class Markdown {
// (https://github.com/vector-im/riot-web/issues/3154)
softbreak: '<br />',
});
// Trying to strip out the wrapping <p/> causes a lot more complication
// than it's worth, i think. For instance, this code will go and strip
// out any <p/> tag (no matter where it is in the tree) which doesn't
// contain \n's.
// On the flip side, <p/>s are quite opionated and restricted on where
// you can nest them.
//
// Let's try sending with <p/>s anyway for now, though.
const real_paragraph = renderer.paragraph;
renderer.paragraph = function(node, entering) {
@ -115,15 +125,20 @@ export default class Markdown {
}
};
renderer.html_inline = html_if_tag_allowed;
renderer.html_block = function(node) {
/*
// as with `paragraph`, we only insert line breaks
// if there are multiple lines in the markdown.
const isMultiLine = is_multi_line(node);
if (isMultiLine) this.cr();
*/
html_if_tag_allowed.call(this, node);
/*
if (isMultiLine) this.cr();
*/
};
return renderer.render(this.parsed);
@ -133,7 +148,10 @@ export default class Markdown {
* Render the markdown message to plain text. That is, essentially
* just remove any backslashes escaping what would otherwise be
* markdown syntax
* (to fix https://github.com/vector-im/riot-web/issues/2870)
* (to fix https://github.com/vector-im/riot-web/issues/2870).
*
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
* which has no formatting. Otherwise it emits HTML(!).
*/
toPlaintext() {
const renderer = new commonmark.HtmlRenderer({safe: false});
@ -156,6 +174,7 @@ export default class Markdown {
}
}
};
renderer.html_block = function(node) {
this.lit(node.literal);
if (is_multi_line(node) && node.next) this.lit('\n\n');

View file

@ -18,6 +18,8 @@ limitations under the License.
'use strict';
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
@ -51,6 +53,9 @@ class MatrixClientPeg {
this.opts = {
initialSyncLimit: 20,
};
// the credentials used to init the current client object.
// used if we tear it down & recreate it with a different store
this._currentClientCreds = null;
}
/**
@ -79,10 +84,30 @@ class MatrixClientPeg {
* Home Server / Identity Server URLs and active credentials
*/
replaceUsingCreds(creds: MatrixClientCreds) {
this._currentClientCreds = creds;
this._createClient(creds);
}
async start() {
for (const dbType of ['indexeddb', 'memory']) {
try {
const promise = this.matrixClient.store.startup();
console.log("MatrixClientPeg: waiting for MatrixClient store to initialise");
await promise;
break;
} catch (err) {
if (dbType === 'indexeddb') {
console.error('Error starting matrixclient store - falling back to memory store', err);
this.matrixClient.store = new Matrix.MatrixInMemoryStore({
localStorage: global.localStorage,
});
} else {
console.error('Failed to start memory store!', err);
throw err;
}
}
}
// try to initialise e2e on the new client
try {
// check that we have a version of the js-sdk which includes initCrypto
@ -99,23 +124,15 @@ class MatrixClientPeg {
// the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached";
try {
const promise = this.matrixClient.store.startup();
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
await promise;
} catch (err) {
// log any errors when starting up the database (if one exists)
console.error(`Error starting matrixclient store: ${err}`);
if (SettingsStore.isFeatureEnabled('feature_lazyloading')) {
opts.lazyLoadMembers = true;
}
// regardless of errors, start the client. If we did error out, we'll
// just end up doing a full initial /sync.
// Connect the matrix client to the dispatcher
MatrixActionCreators.start(this.matrixClient);
console.log(`MatrixClientPeg: really starting MatrixClient`);
this.get().startClient(opts);
await this.get().startClient(opts);
console.log(`MatrixClientPeg: MatrixClient started`);
}
@ -143,7 +160,7 @@ class MatrixClientPeg {
return matches[1];
}
_createClient(creds: MatrixClientCreds) {
_createClient(creds: MatrixClientCreds, useIndexedDb) {
const opts = {
baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl,
@ -154,7 +171,7 @@ class MatrixClientPeg {
forceTURN: SettingsStore.getValue('webRtcForceTURN', false),
};
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
this.matrixClient = createMatrixClient(opts, useIndexedDb);
// we're going to add eventlisteners for each matrix event tile, so the
// potential number of event listeners is quite high.

View file

@ -170,15 +170,15 @@ const Notifier = {
value: true,
});
});
// clear the notifications_hidden flag, so that if notifications are
// disabled again in the future, we will show the banner again.
this.setToolbarHidden(true);
} else {
dis.dispatch({
action: "notifier_enabled",
value: false,
});
}
// set the notifications_hidden flag, as the user has knowingly interacted
// with the setting we shouldn't nag them any further
this.setToolbarHidden(true);
},
isEnabled: function() {

92
src/Registration.js Normal file
View file

@ -0,0 +1,92 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Utility code for registering with a homeserver
* Note that this is currently *not* used by the actual
* registration code.
*/
import dis from './dispatcher';
import sdk from './index';
import MatrixClientPeg from './MatrixClientPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
/**
* Starts either the ILAG or full registration flow, depending
* on what the HS supports
*
* @param {object} options
* @param {bool} options.go_home_on_cancel If true, goes to
* the hame page if the user cancels the action
*/
export async function startAnyRegistrationFlow(options) {
if (options === undefined) options = {};
const flows = await _getRegistrationFlows();
// look for an ILAG compatible flow. We define this as one
// which has only dummy or recaptcha flows. In practice it
// would support any stage InteractiveAuth supports, just not
// ones like email & msisdn which require the user to supply
// the relevant details in advance. We err on the side of
// caution though.
const hasIlagFlow = flows.some((flow) => {
return flow.stages.every((stage) => {
return ['m.login.dummy', 'm.login.recaptcha'].includes(stage);
});
});
if (hasIlagFlow) {
dis.dispatch({
action: 'view_set_mxid',
go_home_on_cancel: options.go_home_on_cancel,
});
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
title: _t("Registration Required"),
description: _t("You need to register to do this. Would you like to register now?"),
button: _t("Register"),
onFinished: (proceed) => {
if (proceed) {
dis.dispatch({action: 'start_registration'});
} else if (options.go_home_on_cancel) {
dis.dispatch({action: 'view_home_page'});
}
},
});
}
}
async function _getRegistrationFlows() {
try {
await MatrixClientPeg.get().register(
null,
null,
undefined,
{},
{},
);
console.log("Register request succeeded when it should have returned 401!");
} catch (e) {
if (e.httpStatus === 401) {
return e.data.flows;
}
throw e;
}
throw new Error("Register request succeeded when it should have returned 401!");
}

View file

@ -1,307 +1,40 @@
import React from 'react';
import {
Editor,
EditorState,
Modifier,
ContentState,
ContentBlock,
convertFromHTML,
DefaultDraftBlockRenderMap,
DefaultDraftInlineStyle,
CompositeDecorator,
SelectionState,
Entity,
} from 'draft-js';
import * as sdk from './index';
/*
Copyright 2015 - 2017 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as emojione from 'emojione';
import {stateToHTML} from 'draft-js-export-html';
import {SelectionRange} from "./autocomplete/Autocompleter";
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
const MARKDOWN_REGEX = {
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
ITALIC: /([\*_])([\w\s]+?)\1/g,
BOLD: /([\*_])\1([\w\s]+?)\1\1/g,
HR: /(\n|^)((-|\*|_) *){3,}(\n|$)/g,
CODE: /`[^`]*`/g,
STRIKETHROUGH: /~{2}[^~]*~{2}/g,
};
const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
export function unicodeToEmojiUri(str) {
const mappedUnicode = emojione.mapUnicodeToShort();
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
export function stateToMarkdown(state) {
return __stateToMarkdown(state)
.replace(
ZWS, // draft-js-export-markdown adds these
''); // this is *not* a zero width space, trust me :)
}
export const contentStateToHTML = (contentState: ContentState) => {
return stateToHTML(contentState, {
inlineStyles: {
UNDERLINE: {
element: 'u',
},
},
});
};
export function htmlToContentState(html: string): ContentState {
const blockArray = convertFromHTML(html).contentBlocks;
return ContentState.createFromBlockArray(blockArray);
}
function unicodeToEmojiUri(str) {
let replaceWith, unicode, alt;
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
const mappedUnicode = emojione.mapUnicodeToShort();
}
str = str.replace(emojione.regUnicode, function(unicodeChar) {
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
// if the unicodeChar doesnt exist just return the entire match
// remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them
return str.replace(emojione.regUnicode, function(unicodeChar) {
if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) {
// if the unicodeChar doesn't exist just return the entire match
return unicodeChar;
} else {
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
if (unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
unicodeChar = unicodeChar[0];
}
// get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar];
const unicode = emojione.jsEscapeMap[unicodeChar];
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
const short = mappedUnicode[unicode];
const fname = emojione.emojioneList[short].fname;
return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam;
}
});
return str;
}
/**
* Utility function that looks for regex matches within a ContentBlock and invokes {callback} with (start, end)
* From https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html
*/
function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: number, end: number) => any) {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
}
// Workaround for https://github.com/facebook/draft-js/issues/414
const emojiDecorator = {
strategy: (contentState, contentBlock, callback) => {
findWithRegex(EMOJI_REGEX, contentBlock, callback);
},
component: (props) => {
const uri = unicodeToEmojiUri(props.children[0].props.text);
const shortname = emojione.toShort(props.children[0].props.text);
const style = {
display: 'inline-block',
width: '1em',
maxHeight: '1em',
background: `url(${uri})`,
backgroundSize: 'contain',
backgroundPosition: 'center center',
overflow: 'hidden',
};
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{ props.children }</span></span>);
},
};
/**
* Returns a composite decorator which has access to provided scope.
*/
export function getScopedRTDecorators(scope: any): CompositeDecorator {
return [emojiDecorator];
}
export function getScopedMDDecorators(scope: any): CompositeDecorator {
const markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
(style) => ({
strategy: (contentState, contentBlock, callback) => {
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
},
component: (props) => (
<span className={"mx_MarkdownElement mx_Markdown_" + style}>
{ props.children }
</span>
),
}));
markdownDecorators.push({
strategy: (contentState, contentBlock, callback) => {
return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback);
},
component: (props) => (
<a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
{ props.children }
</a>
),
});
// markdownDecorators.push(emojiDecorator);
// TODO Consider renabling "syntax highlighting" when we can do it properly
return [emojiDecorator];
}
/**
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
*/
export function modifyText(contentState: ContentState, rangeToReplace: SelectionState,
modifyFn: (text: string) => string, inlineStyle, entityKey): ContentState {
let getText = (key) => contentState.getBlockForKey(key).getText(),
startKey = rangeToReplace.getStartKey(),
startOffset = rangeToReplace.getStartOffset(),
endKey = rangeToReplace.getEndKey(),
endOffset = rangeToReplace.getEndOffset(),
text = "";
for (let currentKey = startKey;
currentKey && currentKey !== endKey;
currentKey = contentState.getKeyAfter(currentKey)) {
const blockText = getText(currentKey);
text += blockText.substring(startOffset, blockText.length);
// from now on, we'll take whole blocks
startOffset = 0;
}
// add remaining part of last block
text += getText(endKey).substring(startOffset, endOffset);
return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), inlineStyle, entityKey);
}
/**
* Computes the plaintext offsets of the given SelectionState.
* Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
* Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
*/
export function selectionStateToTextOffsets(selectionState: SelectionState,
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
let offset = 0, start = 0, end = 0;
for (const block of contentBlocks) {
if (selectionState.getStartKey() === block.getKey()) {
start = offset + selectionState.getStartOffset();
}
if (selectionState.getEndKey() === block.getKey()) {
end = offset + selectionState.getEndOffset();
break;
}
offset += block.getLength();
}
return {
start,
end,
};
}
export function textOffsetsToSelectionState({start, end}: SelectionRange,
contentBlocks: Array<ContentBlock>): SelectionState {
let selectionState = SelectionState.createEmpty();
// Subtract block lengths from `start` and `end` until they are less than the current
// block length (accounting for the NL at the end of each block). Set them to -1 to
// indicate that the corresponding selection state has been determined.
for (const block of contentBlocks) {
const blockLength = block.getLength();
// -1 indicating that the position start position has been found
if (start !== -1) {
if (start < blockLength + 1) {
selectionState = selectionState.merge({
anchorKey: block.getKey(),
anchorOffset: start,
});
start = -1; // selection state for the start calculated
} else {
start -= blockLength + 1; // +1 to account for newline between blocks
}
}
// -1 indicating that the position end position has been found
if (end !== -1) {
if (end < blockLength + 1) {
selectionState = selectionState.merge({
focusKey: block.getKey(),
focusOffset: end,
});
end = -1; // selection state for the end calculated
} else {
end -= blockLength + 1; // +1 to account for newline between blocks
}
}
}
return selectionState;
}
// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState {
const contentState = editorState.getCurrentContent();
const blocks = contentState.getBlockMap();
let newContentState = contentState;
blocks.forEach((block) => {
const plainText = block.getText();
const addEntityToEmoji = (start, end) => {
const existingEntityKey = block.getEntityAt(start);
if (existingEntityKey) {
// avoid manipulation in case the emoji already has an entity
const entity = newContentState.getEntity(existingEntityKey);
if (entity && entity.get('type') === 'emoji') {
return;
}
}
const selection = SelectionState.createEmpty(block.getKey())
.set('anchorOffset', start)
.set('focusOffset', end);
const emojiText = plainText.substring(start, end);
newContentState = newContentState.createEntity(
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
);
const entityKey = newContentState.getLastCreatedEntityKey();
newContentState = Modifier.replaceText(
newContentState,
selection,
emojiText,
null,
entityKey,
);
};
findWithRegex(EMOJI_REGEX, block, addEntityToEmoji);
});
if (!newContentState.equals(contentState)) {
const oldSelection = editorState.getSelection();
editorState = EditorState.push(
editorState,
newContentState,
'convert-to-immutable-emojis',
);
// this is somewhat of a hack, we're undoing selection changes caused above
// it would be better not to make those changes in the first place
editorState = EditorState.forceSelection(editorState, oldSelection);
}
return editorState;
}
export function hasMultiLineSelection(editorState: EditorState): boolean {
const selectionState = editorState.getSelection();
const anchorKey = selectionState.getAnchorKey();
const currentContent = editorState.getCurrentContent();
const currentContentBlock = currentContent.getBlockForKey(anchorKey);
const start = selectionState.getStartOffset();
const end = selectionState.getEndOffset();
const selectedText = currentContentBlock.getText().slice(start, end);
return selectedText.includes('\n');
}

View file

@ -191,14 +191,10 @@ function _showAnyInviteErrors(addrs, room) {
function _getDirectMessageRooms(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
const rooms = [];
dmRooms.forEach((dmRoom) => {
const rooms = dmRooms.filter((dmRoom) => {
const room = MatrixClientPeg.get().getRoom(dmRoom);
if (room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
rooms.push(room);
}
return room.getMyMembership() === 'join';
}
});
return rooms;

View file

@ -31,27 +31,27 @@ export function getDisplayAliasForRoom(room) {
* If the room contains only two members including the logged-in user,
* return the other one. Otherwise, return null.
*/
export function getOnlyOtherMember(room, me) {
const joinedMembers = room.getJoinedMembers();
export function getOnlyOtherMember(room, myUserId) {
if (joinedMembers.length === 2) {
return joinedMembers.filter(function(m) {
return m.userId !== me.userId;
if (room.currentState.getJoinedMemberCount() === 2) {
return room.getJoinedMembers().filter(function(m) {
return m.userId !== myUserId;
})[0];
}
return null;
}
function _isConfCallRoom(room, me, conferenceHandler) {
function _isConfCallRoom(room, myUserId, conferenceHandler) {
if (!conferenceHandler) return false;
if (me.membership != "join") {
const myMembership = room.getMyMembership();
if (myMembership != "join") {
return false;
}
const otherMember = getOnlyOtherMember(room, me);
if (otherMember === null) {
const otherMember = getOnlyOtherMember(room, myUserId);
if (!otherMember) {
return false;
}
@ -68,29 +68,31 @@ const isConfCallRoomCache = {
// $roomId: bool
};
export function isConfCallRoom(room, me, conferenceHandler) {
export function isConfCallRoom(room, myUserId, conferenceHandler) {
if (isConfCallRoomCache[room.roomId] !== undefined) {
return isConfCallRoomCache[room.roomId];
}
const result = _isConfCallRoom(room, me, conferenceHandler);
const result = _isConfCallRoom(room, myUserId, conferenceHandler);
isConfCallRoomCache[room.roomId] = result;
return result;
}
export function looksLikeDirectMessageRoom(room, me) {
if (me.membership == "join" || me.membership === "ban" ||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
export function looksLikeDirectMessageRoom(room, myUserId) {
const myMembership = room.getMyMembership();
const me = room.getMember(myUserId);
if (myMembership == "join" || myMembership === "ban" || (me && me.isKicked())) {
// Used to split rooms via tags
const tagNames = Object.keys(room.tags);
// Used for 1:1 direct chats
const members = room.currentState.getMembers();
// Show 1:1 chats in seperate "Direct Messages" section as long as they haven't
// been moved to a different tag section
if (members.length === 2 && !tagNames.length) {
const totalMemberCount = room.currentState.getJoinedMemberCount() +
room.currentState.getInvitedMemberCount();
if (totalMemberCount === 2 && !tagNames.length) {
return true;
}
}
@ -100,10 +102,10 @@ export function looksLikeDirectMessageRoom(room, me) {
export function guessAndSetDMRoom(room, isDirect) {
let newTarget;
if (isDirect) {
const guessedTarget = guessDMRoomTarget(
room, room.getMember(MatrixClientPeg.get().credentials.userId),
const guessedUserId = guessDMRoomTargetId(
room, MatrixClientPeg.get().getUserId()
);
newTarget = guessedTarget.userId;
newTarget = guessedUserId;
} else {
newTarget = null;
}
@ -159,15 +161,15 @@ export function setDMRoom(roomId, userId) {
* Given a room, estimate which of its members is likely to
* be the target if the room were a DM room and return that user.
*/
export function guessDMRoomTarget(room, me) {
function guessDMRoomTargetId(room, myUserId) {
let oldestTs;
let oldestUser;
// Pick the joined user who's been here longest (and isn't us),
for (const user of room.getJoinedMembers()) {
if (user.userId == me.userId) continue;
if (user.userId == myUserId) continue;
if (oldestTs === undefined || user.events.member.getTs() < oldestTs) {
if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) {
oldestUser = user;
oldestTs = user.events.member.getTs();
}
@ -176,14 +178,14 @@ export function guessDMRoomTarget(room, me) {
// if there are no joined members other than us, use the oldest member
for (const user of room.currentState.getMembers()) {
if (user.userId == me.userId) continue;
if (user.userId == myUserId) continue;
if (oldestTs === undefined || user.events.member.getTs() < oldestTs) {
if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) {
oldestUser = user;
oldestTs = user.events.member.getTs();
}
}
if (oldestUser === undefined) return me;
if (oldestUser === undefined) return myUserId;
return oldestUser;
}

View file

@ -56,32 +56,31 @@ class ScalarAuthClient {
// Something went wrong - try to get a new token.
console.warn("Registering for new scalar token");
return this.registerForToken();
})
});
}
}
validateToken(token) {
let url = SdkConfig.get().integrations_rest_url + "/account";
const url = SdkConfig.get().integrations_rest_url + "/account";
const defer = Promise.defer();
request({
method: "GET",
uri: url,
qs: {scalar_token: token},
json: true,
}, (err, response, body) => {
if (err) {
defer.reject(err);
} else if (response.statusCode / 100 !== 2) {
defer.reject({statusCode: response.statusCode});
} else if (!body || !body.user_id) {
defer.reject(new Error("Missing user_id in response"));
} else {
defer.resolve(body.user_id);
}
return new Promise(function(resolve, reject) {
request({
method: "GET",
uri: url,
qs: {scalar_token: token},
json: true,
}, (err, response, body) => {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode});
} else if (!body || !body.user_id) {
reject(new Error("Missing user_id in response"));
} else {
resolve(body.user_id);
}
});
});
return defer.promise;
}
registerForToken() {
@ -96,56 +95,54 @@ class ScalarAuthClient {
}
exchangeForScalarToken(openid_token_object) {
const defer = Promise.defer();
const scalar_rest_url = SdkConfig.get().integrations_rest_url;
request({
method: 'POST',
uri: scalar_rest_url+'/register',
body: openid_token_object,
json: true,
}, (err, response, body) => {
if (err) {
defer.reject(err);
} else if (response.statusCode / 100 !== 2) {
defer.reject({statusCode: response.statusCode});
} else if (!body || !body.scalar_token) {
defer.reject(new Error("Missing scalar_token in response"));
} else {
defer.resolve(body.scalar_token);
}
});
return defer.promise;
return new Promise(function(resolve, reject) {
request({
method: 'POST',
uri: scalar_rest_url+'/register',
body: openid_token_object,
json: true,
}, (err, response, body) => {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode});
} else if (!body || !body.scalar_token) {
reject(new Error("Missing scalar_token in response"));
} else {
resolve(body.scalar_token);
}
});
});
}
getScalarPageTitle(url) {
const defer = Promise.defer();
let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
request({
method: 'GET',
uri: scalarPageLookupUrl,
json: true,
}, (err, response, body) => {
if (err) {
defer.reject(err);
} else if (response.statusCode / 100 !== 2) {
defer.reject({statusCode: response.statusCode});
} else if (!body) {
defer.reject(new Error("Missing page title in response"));
} else {
let title = "";
if (body.page_title_cache_item && body.page_title_cache_item.cached_title) {
title = body.page_title_cache_item.cached_title;
}
defer.resolve(title);
}
});
return defer.promise;
return new Promise(function(resolve, reject) {
request({
method: 'GET',
uri: scalarPageLookupUrl,
json: true,
}, (err, response, body) => {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode});
} else if (!body) {
reject(new Error("Missing page title in response"));
} else {
let title = "";
if (body.page_title_cache_item && body.page_title_cache_item.cached_title) {
title = body.page_title_cache_item.cached_title;
}
resolve(title);
}
});
});
}
/**

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -231,11 +232,12 @@ Example:
}
*/
const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher");
const Widgets = require('./utils/widgets');
import SdkConfig from './SdkConfig';
import MatrixClientPeg from './MatrixClientPeg';
import { MatrixEvent } from 'matrix-js-sdk';
import dis from './dispatcher';
import WidgetUtils from './utils/WidgetUtils';
import RoomViewStore from './stores/RoomViewStore';
import { _t } from './languageHandler';
function sendResponse(event, res) {
@ -286,51 +288,6 @@ function inviteUser(event, roomId, userId) {
});
}
/**
* Returns a promise that resolves when a widget with the given
* ID has been added as a user widget (ie. the accountData event
* arrives) or rejects after a timeout
*
* @param {string} widgetId The ID of the widget to wait for
* @param {boolean} add True to wait for the widget to be added,
* false to wait for it to be deleted.
* @returns {Promise} that resolves when the widget is available
*/
function waitForUserWidget(widgetId, add) {
return new Promise((resolve, reject) => {
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
// Tests an account data event, returning true if it's in the state
// we're waiting for it to be in
function eventInIntendedState(ev) {
if (!ev || !currentAccountDataEvent.getContent()) return false;
if (add) {
return ev.getContent()[widgetId] !== undefined;
} else {
return ev.getContent()[widgetId] === undefined;
}
}
if (eventInIntendedState(currentAccountDataEvent)) {
resolve();
return;
}
function onAccountData(ev) {
if (eventInIntendedState(currentAccountDataEvent)) {
MatrixClientPeg.get().removeListener('accountData', onAccountData);
clearTimeout(timerId);
resolve();
}
}
const timerId = setTimeout(() => {
MatrixClientPeg.get().removeListener('accountData', onAccountData);
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
}, 10000);
MatrixClientPeg.get().on('accountData', onAccountData);
});
}
function setWidget(event, roomId) {
const widgetId = event.data.widget_id;
const widgetType = event.data.type;
@ -339,12 +296,6 @@ function setWidget(event, roomId) {
const widgetData = event.data.data; // optional
const userWidget = event.data.userWidget;
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
return;
}
// both adding/removing widgets need these checks
if (!widgetId || widgetUrl === undefined) {
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
@ -371,42 +322,8 @@ function setWidget(event, roomId) {
}
}
let content = {
type: widgetType,
url: widgetUrl,
name: widgetName,
data: widgetData,
};
if (userWidget) {
const client = MatrixClientPeg.get();
const userWidgets = Widgets.getUserWidgets();
// Delete existing widget with ID
try {
delete userWidgets[widgetId];
} catch (e) {
console.error(`$widgetId is non-configurable`);
}
// Add new widget / update
if (widgetUrl !== null) {
userWidgets[widgetId] = {
content: content,
sender: client.getUserId(),
state_key: widgetId,
type: 'm.widget',
id: widgetId,
};
}
// This starts listening for when the echo comes back from the server
// since the widget won't appear added until this happens. If we don't
// wait for this, the action will complete but if the user is fast enough,
// the widget still won't actually be there.
client.setAccountData('m.widgets', userWidgets).then(() => {
return waitForUserWidget(widgetId, widgetUrl !== null);
}).then(() => {
WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
sendResponse(event, {
success: true,
});
@ -419,15 +336,7 @@ function setWidget(event, roomId) {
if (!roomId) {
sendError(event, _t('Missing roomId.'), null);
}
if (widgetUrl === null) { // widget is being deleted
content = {};
}
// TODO - Room widgets need to be moved to 'm.widget' state events
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
// XXX: We should probably wait for the echo of the state event to come back from the server,
// as we do with user widgets.
WidgetUtils.setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
sendResponse(event, {
success: true,
});
@ -451,21 +360,13 @@ function getWidgets(event, roomId) {
sendError(event, _t('This room is not recognised.'));
return;
}
// TODO - Room widgets need to be moved to 'm.widget' state events
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
// Only return widgets which have required fields
if (room) {
stateEvents.forEach((ev) => {
if (ev.getContent().type && ev.getContent().url) {
widgetStateEvents.push(ev.event); // return the raw event
}
});
}
// XXX: This gets the raw event object (I think because we can't
// send the MatrixEvent over postMessage?)
widgetStateEvents = WidgetUtils.getRoomWidgets(room).map((ev) => ev.event);
}
// Add user widgets (not linked to a specific room)
const userWidgets = Widgets.getUserWidgetsArray();
const userWidgets = WidgetUtils.getUserWidgetsArray();
widgetStateEvents = widgetStateEvents.concat(userWidgets);
sendResponse(event, widgetStateEvents);
@ -579,7 +480,7 @@ function getMembershipCount(event, roomId) {
sendError(event, _t('This room is not recognised.'));
return;
}
const count = room.getJoinedMembers().length;
const count = room.getJoinedMemberCount();
sendResponse(event, count);
}
@ -596,12 +497,11 @@ function canSendEvent(event, roomId) {
sendError(event, _t('This room is not recognised.'));
return;
}
const me = client.credentials.userId;
const member = room.getMember(me);
if (!member || member.membership !== "join") {
if (room.getMyMembership() !== "join") {
sendError(event, _t('You are not in this room.'));
return;
}
const me = client.credentials.userId;
let canSend = false;
if (isState) {
@ -637,19 +537,6 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
sendResponse(event, stateEvent.getContent());
}
let currentRoomId = null;
let currentRoomAlias = null;
// Listen for when a room is viewed
dis.register(onAction);
function onAction(payload) {
if (payload.action !== "view_room") {
return;
}
currentRoomId = payload.room_id;
currentRoomAlias = payload.room_alias;
}
const onMessage = function(event) {
if (!event.origin) { // stupid chrome
event.origin = event.originalEvent.origin;
@ -700,80 +587,63 @@ const onMessage = function(event) {
return;
}
}
let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) {
if (!currentRoomAlias) {
sendError(event, _t('Must be viewing a room'));
return;
}
// no room ID but there is an alias, look it up.
console.log("Looking up alias " + currentRoomAlias);
promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
return res.room_id;
});
if (roomId !== RoomViewStore.getRoomId()) {
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
return;
}
promise.then((viewingRoomId) => {
if (roomId !== viewingRoomId) {
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
return;
}
// Get and set room-based widgets
if (event.data.action === "get_widgets") {
getWidgets(event, roomId);
return;
} else if (event.data.action === "set_widget") {
setWidget(event, roomId);
return;
}
// Get and set room-based widgets
if (event.data.action === "get_widgets") {
getWidgets(event, roomId);
return;
} else if (event.data.action === "set_widget") {
setWidget(event, roomId);
return;
}
// These APIs don't require userId
if (event.data.action === "join_rules_state") {
getJoinRules(event, roomId);
return;
} else if (event.data.action === "set_plumbing_state") {
setPlumbingState(event, roomId, event.data.status);
return;
} else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId);
return;
} else if (event.data.action === "get_room_enc_state") {
getRoomEncState(event, roomId);
return;
} else if (event.data.action === "can_send_event") {
canSendEvent(event, roomId);
return;
}
// These APIs don't require userId
if (event.data.action === "join_rules_state") {
getJoinRules(event, roomId);
return;
} else if (event.data.action === "set_plumbing_state") {
setPlumbingState(event, roomId, event.data.status);
return;
} else if (event.data.action === "get_membership_count") {
getMembershipCount(event, roomId);
return;
} else if (event.data.action === "get_room_enc_state") {
getRoomEncState(event, roomId);
return;
} else if (event.data.action === "can_send_event") {
canSendEvent(event, roomId);
return;
}
if (!userId) {
sendError(event, _t('Missing user_id in request'));
return;
}
switch (event.data.action) {
case "membership_state":
getMembershipState(event, roomId, userId);
break;
case "invite":
inviteUser(event, roomId, userId);
break;
case "bot_options":
botOptions(event, roomId, userId);
break;
case "set_bot_options":
setBotOptions(event, roomId, userId);
break;
case "set_bot_power":
setBotPower(event, roomId, userId, event.data.level);
break;
default:
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
break;
}
}, (err) => {
console.error(err);
sendError(event, _t('Failed to lookup current room') + '.');
});
if (!userId) {
sendError(event, _t('Missing user_id in request'));
return;
}
switch (event.data.action) {
case "membership_state":
getMembershipState(event, roomId, userId);
break;
case "invite":
inviteUser(event, roomId, userId);
break;
case "bot_options":
botOptions(event, roomId, userId);
break;
case "set_bot_options":
setBotOptions(event, roomId, userId);
break;
case "set_bot_power":
setBotPower(event, roomId, userId, event.data.level);
break;
default:
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
break;
}
};
let listenerCount = 0;

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,28 +15,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import MatrixClientPeg from "./MatrixClientPeg";
import dis from "./dispatcher";
import Tinter from "./Tinter";
import React from 'react';
import MatrixClientPeg from './MatrixClientPeg';
import dis from './dispatcher';
import Tinter from './Tinter';
import sdk from './index';
import { _t } from './languageHandler';
import {_t, _td} from './languageHandler';
import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
class Command {
constructor(name, paramArgs, runFn) {
this.name = name;
this.paramArgs = paramArgs;
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
this.command = '/' + name;
this.args = args;
this.description = description;
this.runFn = runFn;
this.hideCompletionAfterSpace = hideCompletionAfterSpace;
}
getCommand() {
return "/" + this.name;
return this.command;
}
getCommandWithArgs() {
return this.getCommand() + " " + this.paramArgs;
return this.getCommand() + " " + this.args;
}
run(roomId, args) {
@ -47,16 +52,12 @@ class Command {
}
}
function reject(msg) {
return {
error: msg,
};
function reject(error) {
return {error};
}
function success(promise) {
return {
promise: promise,
};
return {promise};
}
/* Disable the "unexpected this" error for these commands - all of the run
@ -65,352 +66,423 @@ function success(promise) {
/* eslint-disable babel/no-invalid-this */
const commands = {
ddg: new Command("ddg", "<query>", function(roomId, args) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here.
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
title: _t('/ddg is not a command'),
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
});
return success();
export const CommandMap = {
ddg: new Command({
name: 'ddg',
args: '<query>',
description: _td('Searches DuckDuckGo for results'),
runFn: function(roomId, args) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here.
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
title: _t('/ddg is not a command'),
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
});
return success();
},
hideCompletionAfterSpace: true,
}),
// Change your nickname
nick: new Command("nick", "<display_name>", function(roomId, args) {
if (args) {
return success(
MatrixClientPeg.get().setDisplayName(args),
);
}
return reject(this.getUsage());
}),
// Changes the colorscheme of your current room
tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
if (args) {
const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) {
Tinter.tint(matches[1], matches[4]);
const colorScheme = {};
colorScheme.primary_color = matches[1];
if (matches[4]) {
colorScheme.secondary_color = matches[4];
} else {
colorScheme.secondary_color = colorScheme.primary_color;
}
return success(
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
);
nick: new Command({
name: 'nick',
args: '<display_name>',
description: _td('Changes your display nickname'),
runFn: function(roomId, args) {
if (args) {
return success(MatrixClientPeg.get().setDisplayName(args));
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
// Change the room topic
topic: new Command("topic", "<topic>", function(roomId, args) {
if (args) {
return success(
MatrixClientPeg.get().setRoomTopic(roomId, args),
);
}
return reject(this.getUsage());
}),
// Invite a user
invite: new Command("invite", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
return success(
MatrixClientPeg.get().invite(roomId, matches[1]),
);
}
}
return reject(this.getUsage());
}),
// Join a room
join: new Command("join", "#alias:domain", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
let roomAlias = matches[1];
if (roomAlias[0] !== '#') {
return reject(this.getUsage());
}
if (!roomAlias.match(/:/)) {
roomAlias += ':' + MatrixClientPeg.get().getDomain();
}
dis.dispatch({
action: 'view_room',
room_alias: roomAlias,
auto_join: true,
});
return success();
}
}
return reject(this.getUsage());
}),
part: new Command("part", "[#alias:domain]", function(roomId, args) {
let targetRoomId;
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
let roomAlias = matches[1];
if (roomAlias[0] !== '#') {
return reject(this.getUsage());
}
if (!roomAlias.match(/:/)) {
roomAlias += ':' + MatrixClientPeg.get().getDomain();
}
// Try to find a room with this alias
const rooms = MatrixClientPeg.get().getRooms();
for (let i = 0; i < rooms.length; i++) {
const aliasEvents = rooms[i].currentState.getStateEvents(
"m.room.aliases",
);
for (let j = 0; j < aliasEvents.length; j++) {
const aliases = aliasEvents[j].getContent().aliases || [];
for (let k = 0; k < aliases.length; k++) {
if (aliases[k] === roomAlias) {
targetRoomId = rooms[i].roomId;
break;
}
}
if (targetRoomId) { break; }
tint: new Command({
name: 'tint',
args: '<color1> [<color2>]',
description: _td('Changes colour scheme of current room'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(#([\da-fA-F]{3}|[\da-fA-F]{6}))( +(#([\da-fA-F]{3}|[\da-fA-F]{6})))?$/);
if (matches) {
Tinter.tint(matches[1], matches[4]);
const colorScheme = {};
colorScheme.primary_color = matches[1];
if (matches[4]) {
colorScheme.secondary_color = matches[4];
} else {
colorScheme.secondary_color = colorScheme.primary_color;
}
if (targetRoomId) { break; }
}
if (!targetRoomId) {
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
return success(
SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
);
}
}
}
if (!targetRoomId) targetRoomId = roomId;
return success(
MatrixClientPeg.get().leave(targetRoomId).then(
function() {
dis.dispatch({action: 'view_next_room'});
},
),
);
return reject(this.getUsage());
},
}),
// Kick a user from the room with an optional reason
kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return success(
MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
);
topic: new Command({
name: 'topic',
args: '<topic>',
description: _td('Sets the room topic'),
runFn: function(roomId, args) {
if (args) {
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
invite: new Command({
name: 'invite',
args: '<user-id>',
description: _td('Invites user with given id to current room'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
return success(MatrixClientPeg.get().invite(roomId, matches[1]));
}
}
return reject(this.getUsage());
},
}),
join: new Command({
name: 'join',
args: '<room-alias>',
description: _td('Joins room with given alias'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
let roomAlias = matches[1];
if (roomAlias[0] !== '#') return reject(this.getUsage());
if (!roomAlias.includes(':')) {
roomAlias += ':' + MatrixClientPeg.get().getDomain();
}
dis.dispatch({
action: 'view_room',
room_alias: roomAlias,
auto_join: true,
});
return success();
}
}
return reject(this.getUsage());
},
}),
part: new Command({
name: 'part',
args: '[<room-alias>]',
description: _td('Leave room'),
runFn: function(roomId, args) {
const cli = MatrixClientPeg.get();
let targetRoomId;
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
let roomAlias = matches[1];
if (roomAlias[0] !== '#') return reject(this.getUsage());
if (!roomAlias.includes(':')) {
roomAlias += ':' + cli.getDomain();
}
// Try to find a room with this alias
const rooms = cli.getRooms();
for (let i = 0; i < rooms.length; i++) {
const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases');
for (let j = 0; j < aliasEvents.length; j++) {
const aliases = aliasEvents[j].getContent().aliases || [];
for (let k = 0; k < aliases.length; k++) {
if (aliases[k] === roomAlias) {
targetRoomId = rooms[i].roomId;
break;
}
}
if (targetRoomId) break;
}
if (targetRoomId) break;
}
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
}
}
if (!targetRoomId) targetRoomId = roomId;
return success(
cli.leave(targetRoomId).then(function() {
dis.dispatch({action: 'view_next_room'});
}),
);
},
}),
kick: new Command({
name: 'kick',
args: '<user-id> [reason]',
description: _td('Kicks user with given id'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3]));
}
}
return reject(this.getUsage());
},
}),
// Ban a user from the room with an optional reason
ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return success(
MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
);
ban: new Command({
name: 'ban',
args: '<user-id> [reason]',
description: _td('Bans user with given id'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3]));
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
// Unban a user from the room
unban: new Command("unban", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
// Reset the user membership to "leave" to unban him
return success(
MatrixClientPeg.get().unban(roomId, matches[1]),
);
// Unban a user from ythe room
unban: new Command({
name: 'unban',
args: '<user-id>',
description: _td('Unbans user with given id'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
// Reset the user membership to "leave" to unban him
return success(MatrixClientPeg.get().unban(roomId, matches[1]));
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
ignore: new Command("ignore", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
const userId = matches[1];
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
ignoredUsers.push(userId); // de-duped internally in the js-sdk
return success(
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
title: _t("Ignored user"),
description: (
<div>
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
</div>
),
hasCancelButton: false,
});
}),
);
ignore: new Command({
name: 'ignore',
args: '<user-id>',
description: _td('Ignores a user, hiding their messages from you'),
runFn: function(roomId, args) {
if (args) {
const cli = MatrixClientPeg.get();
const matches = args.match(/^(\S+)$/);
if (matches) {
const userId = matches[1];
const ignoredUsers = cli.getIgnoredUsers();
ignoredUsers.push(userId); // de-duped internally in the js-sdk
return success(
cli.setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
title: _t('Ignored user'),
description: <div>
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
</div>,
hasCancelButton: false,
});
}),
);
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
unignore: new Command("unignore", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
const userId = matches[1];
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
if (index !== -1) ignoredUsers.splice(index, 1);
return success(
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
title: _t("Unignored user"),
description: (
<div>
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
</div>
),
hasCancelButton: false,
});
}),
);
unignore: new Command({
name: 'unignore',
args: '<user-id>',
description: _td('Stops ignoring a user, showing their messages going forward'),
runFn: function(roomId, args) {
if (args) {
const cli = MatrixClientPeg.get();
const matches = args.match(/^(\S+)$/);
if (matches) {
const userId = matches[1];
const ignoredUsers = cli.getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
if (index !== -1) ignoredUsers.splice(index, 1);
return success(
cli.setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
title: _t('Unignored user'),
description: <div>
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
</div>,
hasCancelButton: false,
});
}),
);
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
// Define the power level of a user
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
let powerLevel = 50; // default power level for op
if (matches) {
const userId = matches[1];
if (matches.length === 4 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]);
}
if (!isNaN(powerLevel)) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
return reject("Bad room ID: " + roomId);
op: new Command({
name: 'op',
args: '<user-id> [<power-level>]',
description: _td('Define the power level of a user'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
let powerLevel = 50; // default power level for op
if (matches) {
const userId = matches[1];
if (matches.length === 4 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]);
}
if (!isNaN(powerLevel)) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId);
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
}
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
return success(
MatrixClientPeg.get().setPowerLevel(
roomId, userId, powerLevel, powerLevelEvent,
),
);
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
// Reset the power level of a user
deop: new Command("deop", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
return reject("Bad room ID: " + roomId);
}
deop: new Command({
name: 'deop',
args: '<user-id>',
description: _td('Deops user with given id'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId);
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
return success(
MatrixClientPeg.get().setPowerLevel(
roomId, args, undefined, powerLevelEvent,
),
);
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
// Open developer tools
devtools: new Command("devtools", "", function(roomId) {
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
Modal.createDialog(DevtoolsDialog, { roomId });
return success();
devtools: new Command({
name: 'devtools',
description: _td('Opens the Developer Tools dialog'),
runFn: function(roomId) {
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
Modal.createDialog(DevtoolsDialog, {roomId});
return success();
},
}),
// Verify a user, device, and pubkey tuple
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
if (matches) {
const userId = matches[1];
const deviceId = matches[2];
const fingerprint = matches[3];
verify: new Command({
name: 'verify',
args: '<user-id> <device-id> <device-signing-key>',
description: _td('Verifies a user, device, and pubkey tuple'),
runFn: function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
if (matches) {
const cli = MatrixClientPeg.get();
return success(
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
if (!device) {
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
}
const userId = matches[1];
const deviceId = matches[2];
const fingerprint = matches[3];
if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) {
throw new Error(_t(`Device already verified!`));
} else {
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
return success(
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
if (!device) {
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
}
}
if (device.getFingerprint() !== fingerprint) {
const fprint = device.getFingerprint();
throw new Error(
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
' "%(fingerprint)s". This could mean your communications are being intercepted!',
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
}
if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) {
throw new Error(_t('Device already verified!'));
} else {
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
}
}
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
}).then(() => {
// Tell the user we verified everything
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
title: _t("Verified key"),
description: (
<div>
<p>
{
_t("The signing key you provided matches the signing key you received " +
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
{userId: userId, deviceId: deviceId})
}
</p>
</div>
),
hasCancelButton: false,
});
}),
);
if (device.getFingerprint() !== fingerprint) {
const fprint = device.getFingerprint();
throw new Error(
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
'"%(fingerprint)s". This could mean your communications are being intercepted!',
{
fprint,
userId,
deviceId,
fingerprint,
}));
}
return cli.setDeviceVerified(userId, deviceId, true);
}).then(() => {
// Tell the user we verified everything
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
title: _t('Verified key'),
description: <div>
<p>
{
_t('The signing key you provided matches the signing key you received ' +
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
{userId, deviceId})
}
</p>
</div>,
hasCancelButton: false,
});
}),
);
}
}
}
return reject(this.getUsage());
return reject(this.getUsage());
},
}),
// Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
me: new Command({
name: 'me',
args: '<message>',
description: _td('Displays action'),
hideCompletionAfterSpace: true,
}),
discardsession: new Command({
name: 'discardsession',
description: _td('Forces the current outbound group session in an encrypted room to be discarded'),
runFn: function(roomId) {
try {
MatrixClientPeg.get().forceDiscardSession(roomId);
} catch (e) {
return reject(e.message);
}
return success();
},
}),
};
/* eslint-enable babel/no-invalid-this */
@ -419,52 +491,43 @@ const commands = {
// helpful aliases
const aliases = {
j: "join",
newballsplease: "discardsession",
};
module.exports = {
/**
* Process the given text for /commands and perform them.
* @param {string} roomId The room in which the command was performed.
* @param {string} input The raw text input by the user.
* @return {Object|null} An object with the property 'error' if there was an error
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
processInput: function(roomId, input) {
// trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands
input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") {
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
let cmd;
let args;
if (bits) {
cmd = bits[1].substring(1).toLowerCase();
args = bits[3];
} else {
cmd = input;
}
if (cmd === "me") return null;
if (aliases[cmd]) {
cmd = aliases[cmd];
}
if (commands[cmd]) {
return commands[cmd].run(roomId, args);
} else {
return reject(_t("Unrecognised command:") + ' ' + input);
}
}
return null; // not a command
},
getCommandList: function() {
// Return all the commands plus /me and /markdown which aren't handled like normal commands
const cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey];
});
cmds.push(new Command("me", "<action>", function() {}));
cmds.push(new Command("markdown", "<on|off>", function() {}));
/**
* Process the given text for /commands and perform them.
* @param {string} roomId The room in which the command was performed.
* @param {string} input The raw text input by the user.
* @return {Object|null} An object with the property 'error' if there was an error
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
export function processCommandInput(roomId, input) {
// trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands
input = input.replace(/\s+$/, '');
if (input[0] !== '/') return null; // not a command
return cmds;
},
};
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
let cmd;
let args;
if (bits) {
cmd = bits[1].substring(1).toLowerCase();
args = bits[3];
} else {
cmd = input;
}
if (aliases[cmd]) {
cmd = aliases[cmd];
}
if (CommandMap[cmd]) {
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
if (!CommandMap[cmd].runFn) return null;
return CommandMap[cmd].run(roomId, args);
} else {
return reject(_t('Unrecognised command:') + ' ' + input);
}
}

View file

@ -129,6 +129,64 @@ function textForRoomNameEvent(ev) {
});
}
function textForServerACLEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent();
const changes = [];
const current = ev.getContent();
const prev = {
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
allow_ip_literals: !(prevContent.allow_ip_literals === false),
};
let text = "";
if (prev.deny.length === 0 && prev.allow.length === 0) {
text = `${senderDisplayName} set server ACLs for this room: `;
} else {
text = `${senderDisplayName} changed the server ACLs for this room: `;
}
if (!Array.isArray(current.allow)) {
current.allow = [];
}
/* If we know for sure everyone is banned, don't bother showing the diff view */
if (current.allow.length === 0) {
return text + "🎉 All servers are banned from participating! This room can no longer be used.";
}
if (!Array.isArray(current.deny)) {
current.deny = [];
}
const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv));
const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv));
const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv));
const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv));
if (bannedServers.length > 0) {
changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`);
}
if (unbannedServers.length > 0) {
changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`);
}
if (allowedServers.length > 0) {
changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`);
}
if (unallowedServers.length > 0) {
changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`);
}
if (prev.allow_ip_literals !== current.allow_ip_literals) {
const allowban = current.allow_ip_literals ? "allowed" : "banned";
changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`);
}
return text + changes.join(" ");
}
function textForMessageEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body;
@ -140,6 +198,63 @@ function textForMessageEvent(ev) {
return message;
}
function textForRoomAliasesEvent(ev) {
// An alternative implementation of this as a first-class event can be found at
// https://github.com/matrix-org/matrix-react-sdk/blob/dc7212ec2bd12e1917233ed7153b3e0ef529a135/src/components/views/messages/RoomAliasesEvent.js
// This feels a bit overkill though, and it's not clear the i18n really needs it
// so instead it's landing as a simple textual event.
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const oldAliases = ev.getPrevContent().aliases || [];
const newAliases = ev.getContent().aliases || [];
const addedAliases = newAliases.filter((x) => !oldAliases.includes(x));
const removedAliases = oldAliases.filter((x) => !newAliases.includes(x));
if (!addedAliases.length && !removedAliases.length) {
return '';
}
if (addedAliases.length && !removedAliases.length) {
return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', {
senderName: senderName,
count: addedAliases.length,
addedAddresses: addedAliases.join(', '),
});
} else if (!addedAliases.length && removedAliases.length) {
return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', {
senderName: senderName,
count: removedAliases.length,
removedAddresses: removedAliases.join(', '),
});
} else {
return _t(
'%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', {
senderName: senderName,
addedAddresses: addedAliases.join(', '),
removedAddresses: removedAliases.join(', '),
},
);
}
}
function textForCanonicalAliasEvent(ev) {
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const oldAlias = ev.getPrevContent().alias;
const newAlias = ev.getContent().alias;
if (newAlias) {
return _t('%(senderName)s set the main address for this room to %(address)s.', {
senderName: senderName,
address: ev.getContent().alias,
});
} else if (oldAlias) {
return _t('%(senderName)s removed the main address for this room.', {
senderName: senderName,
});
}
}
function textForCallAnswerEvent(event) {
const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
@ -157,6 +272,12 @@ function textForCallHangupEvent(event) {
reason = _t('(could not connect media)');
} else if (eventContent.reason === "invite_timeout") {
reason = _t('(no answer)');
} else if (eventContent.reason === "user hangup") {
// workaround for https://github.com/vector-im/riot-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623
reason = '';
} else {
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
}
@ -301,6 +422,8 @@ const handlers = {
};
const stateHandlers = {
'm.room.aliases': textForRoomAliasesEvent,
'm.room.canonical_alias': textForCanonicalAliasEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
@ -309,6 +432,7 @@ const stateHandlers = {
'm.room.encryption': textForEncryptionEvent,
'm.room.power_levels': textForPowerEvent,
'm.room.pinned_events': textForPinnedEvent,
'm.room.server_acl': textForServerACLEvent,
'im.vector.modular.widgets': textForWidgetEvent,
};

View file

@ -17,9 +17,9 @@ limitations under the License.
"use strict";
import Promise from 'bluebird';
var Matrix = require("matrix-js-sdk");
var Room = Matrix.Room;
var CallHandler = require('./CallHandler');
const Matrix = require("matrix-js-sdk");
const Room = Matrix.Room;
const CallHandler = require('./CallHandler');
// FIXME: this is Riot (Vector) specific code, but will be removed shortly when
// we switch over to jitsi entirely for video conferencing.
@ -28,8 +28,8 @@ var CallHandler = require('./CallHandler');
// This is bad because it prevents people running their own ASes from being used.
// This isn't permanent and will be customisable in the future: see the proposal
// at docs/conferencing.md for more info.
var USER_PREFIX = "fs_";
var DOMAIN = "matrix.org";
const USER_PREFIX = "fs_";
const DOMAIN = "matrix.org";
function ConferenceCall(matrixClient, groupChatRoomId) {
this.client = matrixClient;
@ -38,14 +38,14 @@ function ConferenceCall(matrixClient, groupChatRoomId) {
}
ConferenceCall.prototype.setup = function() {
var self = this;
const self = this;
return this._joinConferenceUser().then(function() {
return self._getConferenceUserRoom();
}).then(function(room) {
// return a call for *this* room to be placed. We also tack on
// confUserId to speed up lookups (else we'd need to loop every room
// looking for a 1:1 room with this conf user ID!)
var call = Matrix.createNewMatrixCall(self.client, room.roomId);
const call = Matrix.createNewMatrixCall(self.client, room.roomId);
call.confUserId = self.confUserId;
call.groupRoomId = self.groupRoomId;
return call;
@ -54,11 +54,11 @@ ConferenceCall.prototype.setup = function() {
ConferenceCall.prototype._joinConferenceUser = function() {
// Make sure the conference user is in the group chat room
var groupRoom = this.client.getRoom(this.groupRoomId);
const groupRoom = this.client.getRoom(this.groupRoomId);
if (!groupRoom) {
return Promise.reject("Bad group room ID");
}
var member = groupRoom.getMember(this.confUserId);
const member = groupRoom.getMember(this.confUserId);
if (member && member.membership === "join") {
return Promise.resolve();
}
@ -67,12 +67,12 @@ ConferenceCall.prototype._joinConferenceUser = function() {
ConferenceCall.prototype._getConferenceUserRoom = function() {
// Use an existing 1:1 with the conference user; else make one
var rooms = this.client.getRooms();
var confRoom = null;
for (var i = 0; i < rooms.length; i++) {
var confUser = rooms[i].getMember(this.confUserId);
const rooms = this.client.getRooms();
let confRoom = null;
for (let i = 0; i < rooms.length; i++) {
const confUser = rooms[i].getMember(this.confUserId);
if (confUser && confUser.membership === "join" &&
rooms[i].getJoinedMembers().length === 2) {
rooms[i].getJoinedMemberCount() === 2) {
confRoom = rooms[i];
break;
}
@ -82,9 +82,9 @@ ConferenceCall.prototype._getConferenceUserRoom = function() {
}
return this.client.createRoom({
preset: "private_chat",
invite: [this.confUserId]
invite: [this.confUserId],
}).then(function(res) {
return new Room(res.room_id);
return new Room(res.room_id, null, client.getUserId());
});
};
@ -97,9 +97,9 @@ module.exports.isConferenceUser = function(userId) {
if (userId.indexOf("@" + USER_PREFIX) !== 0) {
return false;
}
var base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length);
const base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length);
if (base64part) {
var decoded = new Buffer(base64part, "base64").toString();
const decoded = new Buffer(base64part, "base64").toString();
// ! $STUFF : $STUFF
return /^!.+:.+/.test(decoded);
}
@ -108,23 +108,23 @@ module.exports.isConferenceUser = function(userId) {
module.exports.getConferenceUserIdForRoom = function(roomId) {
// abuse browserify's core node Buffer support (strip padding ='s)
var base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, "");
const base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, "");
return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN;
};
module.exports.createNewMatrixCall = function(client, roomId) {
var confCall = new ConferenceCall(
client, roomId
const confCall = new ConferenceCall(
client, roomId,
);
return confCall.setup();
};
module.exports.getConferenceCallForRoom = function(roomId) {
// search for a conference 1:1 call for this group chat room ID
var activeCall = CallHandler.getAnyActiveCall();
const activeCall = CallHandler.getAnyActiveCall();
if (activeCall && activeCall.confUserId) {
var thisRoomConfUserId = module.exports.getConferenceUserIdForRoom(
roomId
const thisRoomConfUserId = module.exports.getConferenceUserIdForRoom(
roomId,
);
if (thisRoomConfUserId === activeCall.confUserId) {
return activeCall;

View file

@ -68,7 +68,9 @@ module.exports = React.createClass({
if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
oldNode.style.visibility = c.props.style.visibility;
}
self.children[c.key] = old;
// clone the old element with the props (and children) of the new element
// so prop updates are still received by the children.
self.children[c.key] = React.cloneElement(old, c.props, c.props.children);
} else {
// new element. If we have a startStyle, use that as the style and go through
// the enter animations

View file

@ -1,58 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MatrixClientPeg from './MatrixClientPeg';
export default class WidgetUtils {
/* Returns true if user is able to send state events to modify widgets in this room
* (Does not apply to non-room-based / user widgets)
* @param roomId -- The ID of the room to check
* @return Boolean -- true if the user can modify widgets in this room
* @throws Error -- specifies the error reason
*/
static canUserModifyWidgets(roomId) {
if (!roomId) {
console.warn('No room ID specified');
return false;
}
const client = MatrixClientPeg.get();
if (!client) {
console.warn('User must be be logged in');
return false;
}
const room = client.getRoom(roomId);
if (!room) {
console.warn(`Room ID ${roomId} is not recognised`);
return false;
}
const me = client.credentials.userId;
if (!me) {
console.warn('Failed to get user ID');
return false;
}
const member = room.getMember(me);
if (!member || member.membership !== "join") {
console.warn(`User ${me} is not in room ${roomId}`);
return false;
}
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
}
}

View file

@ -144,23 +144,25 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
/**
* @typedef RoomMembershipAction
* @type {Object}
* @property {string} action 'MatrixActions.RoomMember.membership'.
* @property {RoomMember} member the member whose membership was updated.
* @property {string} action 'MatrixActions.Room.myMembership'.
* @property {Room} room to room for which the self-membership changed.
* @property {string} membership the new membership
* @property {string} oldMembership the previous membership, can be null.
*/
/**
* Create a MatrixActions.RoomMember.membership action that represents
* a MatrixClient `RoomMember.membership` matrix event, emitted when a
* member's membership is updated.
* Create a MatrixActions.Room.myMembership action that represents
* a MatrixClient `Room.myMembership` event for the syncing user,
* emitted when the syncing user's membership is updated for a room.
*
* @param {MatrixClient} matrixClient the matrix client.
* @param {MatrixEvent} membershipEvent the m.room.member event.
* @param {RoomMember} member the member whose membership was updated.
* @param {string} oldMembership the member's previous membership.
* @returns {RoomMembershipAction} an action of type `MatrixActions.RoomMember.membership`.
* @param {Room} room to room for which the self-membership changed.
* @param {string} membership the new membership
* @param {string} oldMembership the previous membership, can be null.
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
*/
function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) {
return { action: 'MatrixActions.RoomMember.membership', member };
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) {
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership};
}
/**
@ -202,7 +204,7 @@ export default {
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createRoomMembershipAction);
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
},
@ -217,7 +219,10 @@ export default {
*/
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
const listener = (...args) => {
dis.dispatch(actionCreator(matrixClient, ...args), true);
const payload = actionCreator(matrixClient, ...args);
if (payload) {
dis.dispatch(payload, true);
}
};
matrixClient.on(eventName, listener);
this._matrixClientListenersStop.push(() => {

View file

@ -1,7 +1,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -20,13 +20,19 @@ import React from 'react';
import type {Completion, SelectionRange} from './Autocompleter';
export default class AutocompleteProvider {
constructor(commandRegex?: RegExp) {
constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
if (commandRegex) {
if (!commandRegex.global) {
throw new Error('commandRegex must have global flag set');
}
this.commandRegex = commandRegex;
}
if (forcedCommandRegex) {
if (!forcedCommandRegex.global) {
throw new Error('forcedCommandRegex must have global flag set');
}
this.forcedCommandRegex = forcedCommandRegex;
}
}
destroy() {
@ -36,11 +42,11 @@ export default class AutocompleteProvider {
/**
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
*/
getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string {
getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false): ?string {
let commandRegex = this.commandRegex;
if (force && this.shouldForceComplete()) {
commandRegex = /\S+/g;
commandRegex = this.forcedCommandRegex || /\S+/g;
}
if (commandRegex == null) {
@ -51,14 +57,14 @@ export default class AutocompleteProvider {
let match;
while ((match = commandRegex.exec(query)) != null) {
let matchStart = match.index,
matchEnd = matchStart + match[0].length;
if (selection.start <= matchEnd && selection.end >= matchStart) {
const start = match.index;
const end = start + match[0].length;
if (selection.start <= end && selection.end >= start) {
return {
command: match,
range: {
start: matchStart,
end: matchEnd,
start,
end,
},
};
}

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,7 +18,9 @@ limitations under the License.
// @flow
import type {Component} from 'react';
import {Room} from 'matrix-js-sdk';
import CommandProvider from './CommandProvider';
import CommunityProvider from './CommunityProvider';
import DuckDuckGoProvider from './DuckDuckGoProvider';
import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
@ -27,8 +29,9 @@ import NotifProvider from './NotifProvider';
import Promise from 'bluebird';
export type SelectionRange = {
start: number,
end: number
beginning: boolean, // whether the selection is in the first block of the editor or not
start: number, // byte offset relative to the start anchor of the current editor selection.
end: number, // byte offset relative to the end anchor of the current editor selection.
};
export type Completion = {
@ -47,6 +50,7 @@ const PROVIDERS = [
EmojiProvider,
NotifProvider,
CommandProvider,
CommunityProvider,
DuckDuckGoProvider,
];
@ -54,7 +58,7 @@ const PROVIDERS = [
const PROVIDER_COMPLETION_TIMEOUT = 3000;
export default class Autocompleter {
constructor(room) {
constructor(room: Room) {
this.room = room;
this.providers = PROVIDERS.map((p) => {
return new p(room);
@ -77,12 +81,12 @@ export default class Autocompleter {
// Array of inspections of promises that might timeout. Instead of allowing a
// single timeout to reject the Promise.all, reflect each one and once they've all
// settled, filter for the fulfilled ones
this.providers.map((provider) => {
return provider
this.providers.map(provider =>
provider
.getCompletions(query, selection, force)
.timeout(PROVIDER_COMPLETION_TIMEOUT)
.reflect();
}),
.reflect()
),
);
return completionsList.filter(

View file

@ -2,6 +2,7 @@
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,103 +18,16 @@ limitations under the License.
*/
import React from 'react';
import { _t, _td } from '../languageHandler';
import {_t} from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components';
import type {Completion, SelectionRange} from "./Autocompleter";
import {CommandMap} from '../SlashCommands';
// TODO merge this with the factory mechanics of SlashCommands?
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
const COMMANDS = [
{
command: '/me',
args: '<message>',
description: _td('Displays action'),
},
{
command: '/ban',
args: '<user-id> [reason]',
description: _td('Bans user with given id'),
},
{
command: '/unban',
args: '<user-id>',
description: _td('Unbans user with given id'),
},
{
command: '/op',
args: '<user-id> [<power-level>]',
description: _td('Define the power level of a user'),
},
{
command: '/deop',
args: '<user-id>',
description: _td('Deops user with given id'),
},
{
command: '/invite',
args: '<user-id>',
description: _td('Invites user with given id to current room'),
},
{
command: '/join',
args: '<room-alias>',
description: _td('Joins room with given alias'),
},
{
command: '/part',
args: '[<room-alias>]',
description: _td('Leave room'),
},
{
command: '/topic',
args: '<topic>',
description: _td('Sets the room topic'),
},
{
command: '/kick',
args: '<user-id> [reason]',
description: _td('Kicks user with given id'),
},
{
command: '/nick',
args: '<display-name>',
description: _td('Changes your display nickname'),
},
{
command: '/ddg',
args: '<query>',
description: _td('Searches DuckDuckGo for results'),
},
{
command: '/tint',
args: '<color1> [<color2>]',
description: _td('Changes colour scheme of current room'),
},
{
command: '/verify',
args: '<user-id> <device-id> <device-signing-key>',
description: _td('Verifies a user, device, and pubkey tuple'),
},
{
command: '/ignore',
args: '<user-id>',
description: _td('Ignores a user, hiding their messages from you'),
},
{
command: '/unignore',
args: '<user-id>',
description: _td('Stops ignoring a user, showing their messages going forward'),
},
{
command: '/devtools',
args: '',
description: _td('Opens the Developer Tools dialog'),
},
// Omitting `/markdown` as it only seems to apply to OldComposer
];
const COMMANDS = Object.values(CommandMap);
const COMMAND_RE = /(^\/\w*)/g;
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
export default class CommandProvider extends AutocompleteProvider {
constructor() {
@ -123,23 +37,39 @@ export default class CommandProvider extends AutocompleteProvider {
});
}
async getCompletions(query: string, selection: {start: number, end: number}) {
let completions = [];
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
completions = this.matcher.match(command[0]).map((result) => {
return {
completion: result.command + ' ',
component: (<TextualCompletion
title={result.command}
subtitle={result.args}
description={_t(result.description)}
/>),
range,
};
});
if (!command) return [];
let matches = [];
// check if the full match differs from the first word (i.e. returns false if the command has args)
if (command[0] !== command[1]) {
// The input looks like a command with arguments, perform exact match
const name = command[1].substr(1); // strip leading `/`
if (CommandMap[name]) {
// some commands, namely `me` and `ddg` don't suit having the usage shown whilst typing their arguments
if (CommandMap[name].hideCompletionAfterSpace) return [];
matches = [CommandMap[name]];
}
} else {
if (query === '/') {
// If they have just entered `/` show everything
matches = COMMANDS;
} else {
// otherwise fuzzy match against all of the fields
matches = this.matcher.match(command[1]);
}
}
return completions;
return matches.map((result) => ({
// If the command is the same as the one they entered, we don't want to discard their arguments
completion: result.command === command[1] ? command[0] : (result.command + ' '),
component: <TextualCompletion
title={result.command}
subtitle={result.args}
description={_t(result.description)} />,
range,
}));
}
getName() {

View file

@ -0,0 +1,111 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import MatrixClientPeg from '../MatrixClientPeg';
import FuzzyMatcher from './FuzzyMatcher';
import {PillCompletion} from './Components';
import sdk from '../index';
import _sortBy from 'lodash/sortBy';
import {makeGroupPermalink} from "../matrix-to";
import type {Completion, SelectionRange} from "./Autocompleter";
import FlairStore from "../stores/FlairStore";
const COMMUNITY_REGEX = /\B\+\S*/g;
function score(query, space) {
const index = space.indexOf(query);
if (index === -1) {
return Infinity;
} else {
return index;
}
}
export default class CommunityProvider extends AutocompleteProvider {
constructor() {
super(COMMUNITY_REGEX);
this.matcher = new FuzzyMatcher([], {
keys: ['groupId', 'name', 'shortDescription'],
});
}
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
// Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/riot-web/issues/4762)
if (/^(\/join|\/leave)/.test(query)) {
return [];
}
const cli = MatrixClientPeg.get();
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) {
const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join');
const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => {
try {
return FlairStore.getGroupProfileCached(cli, groupId);
} catch (e) { // if FlairStore failed, fall back to just groupId
return Promise.resolve({
name: '',
groupId,
avatarUrl: '',
shortDescription: '',
});
}
})));
this.matcher.setObjects(groups);
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = _sortBy(completions, [
(c) => score(matchedString, c.groupId),
(c) => c.groupId.length,
]).map(({avatarUrl, groupId, name}) => ({
completion: groupId,
suffix: ' ',
href: makeGroupPermalink(groupId),
component: (
<PillCompletion initialComponent={
<BaseAvatar name={name || groupId}
width={24} height={24}
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
} title={name} description={groupId} />
),
range,
}))
.slice(0, 4);
}
return completions;
}
getName() {
return '💬 ' + _t('Communities');
}
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{ completions }
</div>;
}
}

View file

@ -1,7 +1,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import AutocompleteProvider from './AutocompleteProvider';
import 'whatwg-fetch';
import {TextualCompletion} from './Components';
import type {SelectionRange} from "./Autocompleter";
const DDG_REGEX = /\/ddg\s+(.+)$/g;
const REFERRER = 'vector';
@ -36,7 +37,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
}
async getCompletions(query: string, selection: {start: number, end: number}) {
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) {
const {command, range} = this.getCurrentCommand(query, selection);
if (!query || !command) {
return [];

View file

@ -1,7 +1,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,11 +19,11 @@ limitations under the License.
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
import FuzzyMatcher from './FuzzyMatcher';
import sdk from '../index';
import {PillCompletion} from './Components';
import type {SelectionRange, Completion} from './Autocompleter';
import type {Completion, SelectionRange} from './Autocompleter';
import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy';
import SettingsStore from "../settings/SettingsStore";
@ -48,7 +48,7 @@ const CATEGORY_ORDER = [
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
// that we need to support inputting multiple emoji with no space between them.
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:\\w*:?)$', 'g');
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g');
// We also need to match the non-zero-length prefixes to remove them from the final match,
// and update the range so that we don't replace the whitespace or the previous emoji.
@ -65,6 +65,7 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor
return {
name: a.name,
shortname: a.shortname,
aliases: a.aliases ? a.aliases.join(' ') : '',
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
// Include the index so that we can preserve the original order
_orderBy: index,
@ -84,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider {
constructor() {
super(EMOJI_REGEX);
this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
keys: ['aliases_ascii', 'shortname'],
keys: ['aliases_ascii', 'shortname', 'aliases'],
// For matching against ascii equivalents
shouldMatchWordsOnly: false,
});
@ -95,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
});
}
async getCompletions(query: string, selection: SelectionRange) {
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
return []; // don't give any suggestions if the user doesn't want them
}

View file

@ -20,6 +20,7 @@ import { _t } from '../languageHandler';
import MatrixClientPeg from '../MatrixClientPeg';
import {PillCompletion} from './Components';
import sdk from '../index';
import type {Completion, SelectionRange} from "./Autocompleter";
const AT_ROOM_REGEX = /@\S*/g;
@ -29,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider {
this.room = room;
}
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
async getCompletions(query: string, selection: SelectionRange, force?:boolean = false): Array<Completion> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get();
@ -40,6 +41,7 @@ export default class NotifProvider extends AutocompleteProvider {
if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) {
return [{
completion: '@room',
completionId: '@room',
suffix: ' ',
component: (
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />

View file

@ -0,0 +1,93 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Based originally on slate-plain-serializer
import { Block } from 'slate';
/**
* Plain text serializer, which converts a Slate `value` to a plain text string,
* serializing pills into various different formats as required.
*
* @type {PlainWithPillsSerializer}
*/
class PlainWithPillsSerializer {
/*
* @param {String} options.pillFormat - either 'md', 'plain', 'id'
*/
constructor(options = {}) {
const {
pillFormat = 'plain',
} = options;
this.pillFormat = pillFormat;
}
/**
* Serialize a Slate `value` to a plain text string,
* serializing pills as either MD links, plain text representations or
* ID representations as required.
*
* @param {Value} value
* @return {String}
*/
serialize = value => {
return this._serializeNode(value.document);
}
/**
* Serialize a `node` to plain text.
*
* @param {Node} node
* @return {String}
*/
_serializeNode = node => {
if (
node.object == 'document' ||
(node.object == 'block' && Block.isBlockList(node.nodes))
) {
return node.nodes.map(this._serializeNode).join('\n');
} else if (node.type == 'emoji') {
return node.data.get('emojiUnicode');
} else if (node.type == 'pill') {
const completion = node.data.get('completion');
// over the wire the @room pill is just plaintext
if (completion === '@room') return completion;
switch (this.pillFormat) {
case 'plain':
return completion;
case 'md':
return `[${ completion }](${ node.data.get('href') })`;
case 'id':
return node.data.get('completionId') || completion;
}
} else if (node.nodes) {
return node.nodes.map(this._serializeNode).join('');
} else {
return node.text;
}
}
}
/**
* Export.
*
* @type {PlainWithPillsSerializer}
*/
export default PlainWithPillsSerializer;

View file

@ -1,6 +1,7 @@
//@flow
/*
Copyright 2017 Aviral Dasgupta
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -27,6 +28,10 @@ class KeyMap {
priorityMap = new Map();
}
function stripDiacritics(str: string): string {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
export default class QueryMatcher {
/**
* @param {object[]} objects the objects to perform a match on
@ -46,10 +51,11 @@ export default class QueryMatcher {
objects.forEach((object, i) => {
const keyValues = _at(object, keys);
for (const keyValue of keyValues) {
if (!map.hasOwnProperty(keyValue)) {
map[keyValue] = [];
const key = stripDiacritics(keyValue).toLowerCase();
if (!map.hasOwnProperty(key)) {
map[key] = [];
}
map[keyValue].push(object);
map[key].push(object);
}
keyMap.priorityMap.set(object, i);
});
@ -82,7 +88,7 @@ export default class QueryMatcher {
}
match(query: String): Array<Object> {
query = query.toLowerCase();
query = stripDiacritics(query).toLowerCase();
if (this.options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, '');
}
@ -91,7 +97,7 @@ export default class QueryMatcher {
}
const results = [];
this.keyMap.keys.forEach((key) => {
let resultKey = key.toLowerCase();
let resultKey = key;
if (this.options.shouldMatchWordsOnly) {
resultKey = resultKey.replace(/[^\w]/g, '');
}

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