Merge branch 'develop' into travis/remove-presence
This commit is contained in:
commit
fe2cbc584d
246 changed files with 16884 additions and 4954 deletions
|
@ -8,7 +8,6 @@ src/CallHandler.js
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
src/components/structures/ContextualMenu.js
|
src/components/structures/ContextualMenu.js
|
||||||
src/components/structures/CreateRoom.js
|
src/components/structures/CreateRoom.js
|
||||||
src/components/structures/FilePanel.js
|
|
||||||
src/components/structures/LoggedInView.js
|
src/components/structures/LoggedInView.js
|
||||||
src/components/structures/login/ForgotPassword.js
|
src/components/structures/login/ForgotPassword.js
|
||||||
src/components/structures/login/Login.js
|
src/components/structures/login/Login.js
|
||||||
|
@ -27,16 +26,10 @@ src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
||||||
src/components/views/dialogs/DeactivateAccountDialog.js
|
src/components/views/dialogs/DeactivateAccountDialog.js
|
||||||
src/components/views/dialogs/UnknownDeviceDialog.js
|
src/components/views/dialogs/UnknownDeviceDialog.js
|
||||||
src/components/views/elements/AddressSelector.js
|
src/components/views/elements/AddressSelector.js
|
||||||
src/components/views/elements/CreateRoomButton.js
|
|
||||||
src/components/views/elements/DeviceVerifyButtons.js
|
src/components/views/elements/DeviceVerifyButtons.js
|
||||||
src/components/views/elements/DirectorySearchBox.js
|
src/components/views/elements/DirectorySearchBox.js
|
||||||
src/components/views/elements/EditableText.js
|
src/components/views/elements/EditableText.js
|
||||||
src/components/views/elements/HomeButton.js
|
|
||||||
src/components/views/elements/MemberEventListSummary.js
|
src/components/views/elements/MemberEventListSummary.js
|
||||||
src/components/views/elements/PowerSelector.js
|
|
||||||
src/components/views/elements/RoomDirectoryButton.js
|
|
||||||
src/components/views/elements/SettingsButton.js
|
|
||||||
src/components/views/elements/StartChatButton.js
|
|
||||||
src/components/views/elements/TintableSvg.js
|
src/components/views/elements/TintableSvg.js
|
||||||
src/components/views/elements/UserSelector.js
|
src/components/views/elements/UserSelector.js
|
||||||
src/components/views/login/CountryDropdown.js
|
src/components/views/login/CountryDropdown.js
|
||||||
|
@ -93,7 +86,6 @@ src/RichText.js
|
||||||
src/Roles.js
|
src/Roles.js
|
||||||
src/Rooms.js
|
src/Rooms.js
|
||||||
src/ScalarAuthClient.js
|
src/ScalarAuthClient.js
|
||||||
src/Tinter.js
|
|
||||||
src/UiEffects.js
|
src/UiEffects.js
|
||||||
src/Unread.js
|
src/Unread.js
|
||||||
src/utils/DecryptFile.js
|
src/utils/DecryptFile.js
|
||||||
|
|
|
@ -3,7 +3,10 @@ dist: trusty
|
||||||
|
|
||||||
# we don't need sudo, so can run in a container, which makes startup much
|
# we don't need sudo, so can run in a container, which makes startup much
|
||||||
# quicker.
|
# quicker.
|
||||||
sudo: false
|
#
|
||||||
|
# unfortunately we do temporarily require sudo as a workaround for
|
||||||
|
# https://github.com/travis-ci/travis-ci/issues/8836
|
||||||
|
sudo: required
|
||||||
|
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
|
|
399
CHANGELOG.md
399
CHANGELOG.md
|
@ -1,3 +1,402 @@
|
||||||
|
Changes in [0.12.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.1) (2018-04-11)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0...v0.12.1)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0) (2018-04-11)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.7...v0.12.0)
|
||||||
|
|
||||||
|
* Further improve group joining/leaving feedback
|
||||||
|
[\#1832](https://github.com/matrix-org/matrix-react-sdk/pull/1832)
|
||||||
|
* Cosmetic changes to Communities button
|
||||||
|
|
||||||
|
Changes in [0.12.0-rc.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.7) (2018-04-10)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.6...v0.12.0-rc.7)
|
||||||
|
|
||||||
|
* Reword group setting delay
|
||||||
|
[\#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)
|
||||||
|
|
||||||
|
* Fix group join button not appearing
|
||||||
|
|
||||||
|
Changes in [0.12.0-rc.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.5) (2018-04-09)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.4...v0.12.0-rc.5)
|
||||||
|
|
||||||
|
* Added radio button to set group join policy
|
||||||
|
* Fix to prevent guests from accessing lab features
|
||||||
|
* Fix broken forgot password page
|
||||||
|
* Fix crash when joining a room after peeking
|
||||||
|
|
||||||
|
Changes in [0.12.0-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.4) (2018-03-22)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.3...v0.12.0-rc.4)
|
||||||
|
|
||||||
|
* Fix broken import preventing people tag
|
||||||
|
[\#1811](https://github.com/matrix-org/matrix-react-sdk/pull/1811)
|
||||||
|
|
||||||
|
Changes in [0.12.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.3) (2018-03-20)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.2...v0.12.0-rc.3)
|
||||||
|
|
||||||
|
* Fix room tile badge not disappearing when receiving a read receipt
|
||||||
|
[\#1807](https://github.com/matrix-org/matrix-react-sdk/pull/1807)
|
||||||
|
|
||||||
|
Changes in [0.12.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.2) (2018-03-19)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.12.0-rc.1...v0.12.0-rc.2)
|
||||||
|
|
||||||
|
* Take TagPanel out of labs
|
||||||
|
[\#1805](https://github.com/matrix-org/matrix-react-sdk/pull/1805)
|
||||||
|
|
||||||
|
Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.12.0-rc.1) (2018-03-19)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.4...v0.12.0-rc.1)
|
||||||
|
|
||||||
|
* Remove the message on migrating crypto data
|
||||||
|
[\#1803](https://github.com/matrix-org/matrix-react-sdk/pull/1803)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1804](https://github.com/matrix-org/matrix-react-sdk/pull/1804)
|
||||||
|
* Improve room list performance when receiving messages
|
||||||
|
[\#1801](https://github.com/matrix-org/matrix-react-sdk/pull/1801)
|
||||||
|
* Add change delay warning in GroupView settings
|
||||||
|
[\#1802](https://github.com/matrix-org/matrix-react-sdk/pull/1802)
|
||||||
|
* Only use `dangerouslySetInnerHTML` for HTML messages
|
||||||
|
[\#1799](https://github.com/matrix-org/matrix-react-sdk/pull/1799)
|
||||||
|
* Limit group requests to 3 at once
|
||||||
|
[\#1798](https://github.com/matrix-org/matrix-react-sdk/pull/1798)
|
||||||
|
* Show GroupMemberList after inviting a group member
|
||||||
|
[\#1796](https://github.com/matrix-org/matrix-react-sdk/pull/1796)
|
||||||
|
* Fix syntax fail
|
||||||
|
[\#1794](https://github.com/matrix-org/matrix-react-sdk/pull/1794)
|
||||||
|
* Use TintableSvg for TagPanel clear filter button
|
||||||
|
[\#1793](https://github.com/matrix-org/matrix-react-sdk/pull/1793)
|
||||||
|
* Fix missing space between "...is a" and user ID
|
||||||
|
[\#1792](https://github.com/matrix-org/matrix-react-sdk/pull/1792)
|
||||||
|
* E2E "fudge-button"
|
||||||
|
[\#1791](https://github.com/matrix-org/matrix-react-sdk/pull/1791)
|
||||||
|
* Remove spurious console.trace
|
||||||
|
[\#1790](https://github.com/matrix-org/matrix-react-sdk/pull/1790)
|
||||||
|
* Don't reset the presence timer on every dispatch
|
||||||
|
[\#1789](https://github.com/matrix-org/matrix-react-sdk/pull/1789)
|
||||||
|
* Potentially fix a memory leak in FlairStore
|
||||||
|
[\#1788](https://github.com/matrix-org/matrix-react-sdk/pull/1788)
|
||||||
|
* Implement transparent RoomTile for use in some places
|
||||||
|
[\#1785](https://github.com/matrix-org/matrix-react-sdk/pull/1785)
|
||||||
|
* Fix varying default group avatar colour for given group
|
||||||
|
[\#1784](https://github.com/matrix-org/matrix-react-sdk/pull/1784)
|
||||||
|
* Fix bug where avatar change not reflected in LLP
|
||||||
|
[\#1783](https://github.com/matrix-org/matrix-react-sdk/pull/1783)
|
||||||
|
* Workaround for atlassian/react-beautiful-dnd#273
|
||||||
|
[\#1782](https://github.com/matrix-org/matrix-react-sdk/pull/1782)
|
||||||
|
* Add setting to disable TagPanel
|
||||||
|
[\#1781](https://github.com/matrix-org/matrix-react-sdk/pull/1781)
|
||||||
|
* [DO NOT MERGE] Tests proven to fail
|
||||||
|
[\#1780](https://github.com/matrix-org/matrix-react-sdk/pull/1780)
|
||||||
|
* Fix room power level settings
|
||||||
|
[\#1779](https://github.com/matrix-org/matrix-react-sdk/pull/1779)
|
||||||
|
* fix shouldHideEvent saying an event is a leave/join when a profile ch…
|
||||||
|
[\#1769](https://github.com/matrix-org/matrix-react-sdk/pull/1769)
|
||||||
|
* Add "Did you know:..." microcopy to groups view
|
||||||
|
[\#1777](https://github.com/matrix-org/matrix-react-sdk/pull/1777)
|
||||||
|
* Give emptySubListTip a container for correct bg colour
|
||||||
|
[\#1753](https://github.com/matrix-org/matrix-react-sdk/pull/1753)
|
||||||
|
* Do proper null-checks on decypted events to fix NPEs
|
||||||
|
[\#1776](https://github.com/matrix-org/matrix-react-sdk/pull/1776)
|
||||||
|
* Reorder the RoomListStore lists on Event.decrypted
|
||||||
|
[\#1775](https://github.com/matrix-org/matrix-react-sdk/pull/1775)
|
||||||
|
* Fix bug where global "Never send to unverified..." is ignored
|
||||||
|
[\#1772](https://github.com/matrix-org/matrix-react-sdk/pull/1772)
|
||||||
|
* Fix bug that prevented tint updates
|
||||||
|
[\#1767](https://github.com/matrix-org/matrix-react-sdk/pull/1767)
|
||||||
|
* Fix group member spinner being out of flex order
|
||||||
|
[\#1765](https://github.com/matrix-org/matrix-react-sdk/pull/1765)
|
||||||
|
* Allow widget iframes to request camera and microphone permissions.
|
||||||
|
[\#1766](https://github.com/matrix-org/matrix-react-sdk/pull/1766)
|
||||||
|
* Change icon from "R" to "X"
|
||||||
|
[\#1764](https://github.com/matrix-org/matrix-react-sdk/pull/1764)
|
||||||
|
* Regenerate room lists on Room event
|
||||||
|
[\#1762](https://github.com/matrix-org/matrix-react-sdk/pull/1762)
|
||||||
|
* Fix DMs being marked as with the current user ("me")
|
||||||
|
[\#1761](https://github.com/matrix-org/matrix-react-sdk/pull/1761)
|
||||||
|
* Make RoomListStore aware of Room.timeline events
|
||||||
|
[\#1756](https://github.com/matrix-org/matrix-react-sdk/pull/1756)
|
||||||
|
* improve origin check of ScalarMessaging postmessage API.
|
||||||
|
[\#1760](https://github.com/matrix-org/matrix-react-sdk/pull/1760)
|
||||||
|
* Implement global filter to deselect all tags
|
||||||
|
[\#1759](https://github.com/matrix-org/matrix-react-sdk/pull/1759)
|
||||||
|
* Don't show empty custom tags when filtering tags
|
||||||
|
[\#1758](https://github.com/matrix-org/matrix-react-sdk/pull/1758)
|
||||||
|
* Do not assume that tags have been removed
|
||||||
|
[\#1757](https://github.com/matrix-org/matrix-react-sdk/pull/1757)
|
||||||
|
* Change CSS class for message panel spinner
|
||||||
|
[\#1747](https://github.com/matrix-org/matrix-react-sdk/pull/1747)
|
||||||
|
* Remove RoomListStore listener
|
||||||
|
[\#1752](https://github.com/matrix-org/matrix-react-sdk/pull/1752)
|
||||||
|
* Implement GroupTile avatar dragging to TagPanel
|
||||||
|
[\#1751](https://github.com/matrix-org/matrix-react-sdk/pull/1751)
|
||||||
|
* Fix custom tags not being ordered manually
|
||||||
|
[\#1750](https://github.com/matrix-org/matrix-react-sdk/pull/1750)
|
||||||
|
* Store component state for editors
|
||||||
|
[\#1746](https://github.com/matrix-org/matrix-react-sdk/pull/1746)
|
||||||
|
* Give the login page its spinner back
|
||||||
|
[\#1745](https://github.com/matrix-org/matrix-react-sdk/pull/1745)
|
||||||
|
* Add context menu to TagTile
|
||||||
|
[\#1743](https://github.com/matrix-org/matrix-react-sdk/pull/1743)
|
||||||
|
* If a tag is unrecognised, assume manual ordering
|
||||||
|
[\#1748](https://github.com/matrix-org/matrix-react-sdk/pull/1748)
|
||||||
|
* Move RoomList state to RoomListStore
|
||||||
|
[\#1719](https://github.com/matrix-org/matrix-react-sdk/pull/1719)
|
||||||
|
* Move groups button to TagPanel
|
||||||
|
[\#1744](https://github.com/matrix-org/matrix-react-sdk/pull/1744)
|
||||||
|
* Add seconds to timestamp on hover
|
||||||
|
[\#1738](https://github.com/matrix-org/matrix-react-sdk/pull/1738)
|
||||||
|
* Do not truncate autocompleted users in composer
|
||||||
|
[\#1739](https://github.com/matrix-org/matrix-react-sdk/pull/1739)
|
||||||
|
* RoomView: guard against unmounting during peeking
|
||||||
|
[\#1737](https://github.com/matrix-org/matrix-react-sdk/pull/1737)
|
||||||
|
* Fix HS/IS URL reset when switching to Registration
|
||||||
|
[\#1736](https://github.com/matrix-org/matrix-react-sdk/pull/1736)
|
||||||
|
* Fix the reject/accept call buttons in canary (mk2)
|
||||||
|
[\#1734](https://github.com/matrix-org/matrix-react-sdk/pull/1734)
|
||||||
|
* Make ratelimitedfunc time from the function's end
|
||||||
|
[\#1731](https://github.com/matrix-org/matrix-react-sdk/pull/1731)
|
||||||
|
* Give dialogs a matrixClient context
|
||||||
|
[\#1735](https://github.com/matrix-org/matrix-react-sdk/pull/1735)
|
||||||
|
* Fix key bindings in address picker dialog
|
||||||
|
[\#1732](https://github.com/matrix-org/matrix-react-sdk/pull/1732)
|
||||||
|
* Try upgrading eslint-plugin-react
|
||||||
|
[\#1712](https://github.com/matrix-org/matrix-react-sdk/pull/1712)
|
||||||
|
* Fix display name change text
|
||||||
|
[\#1730](https://github.com/matrix-org/matrix-react-sdk/pull/1730)
|
||||||
|
* Persist contentState when sending SlashCommand via MessageComposerInput
|
||||||
|
[\#1721](https://github.com/matrix-org/matrix-react-sdk/pull/1721)
|
||||||
|
* This is actually MFileBody not MImageBody, change classname
|
||||||
|
[\#1726](https://github.com/matrix-org/matrix-react-sdk/pull/1726)
|
||||||
|
* Use invite_3pid prop of createRoom instead of manual invite after create
|
||||||
|
[\#1717](https://github.com/matrix-org/matrix-react-sdk/pull/1717)
|
||||||
|
* guard against m.room.aliases events with no keys (redaction?)
|
||||||
|
[\#1729](https://github.com/matrix-org/matrix-react-sdk/pull/1729)
|
||||||
|
* Fix not showing Invited section if all invites are 3PID
|
||||||
|
[\#1718](https://github.com/matrix-org/matrix-react-sdk/pull/1718)
|
||||||
|
* Fix Rich Replies on files
|
||||||
|
[\#1720](https://github.com/matrix-org/matrix-react-sdk/pull/1720)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1728](https://github.com/matrix-org/matrix-react-sdk/pull/1728)
|
||||||
|
* Null guard against falsey (non-null) props.node, to make react happy
|
||||||
|
[\#1724](https://github.com/matrix-org/matrix-react-sdk/pull/1724)
|
||||||
|
* Use correct condition for getting account data after first sync
|
||||||
|
[\#1722](https://github.com/matrix-org/matrix-react-sdk/pull/1722)
|
||||||
|
* Fix order calculation logic when reordering a room
|
||||||
|
[\#1725](https://github.com/matrix-org/matrix-react-sdk/pull/1725)
|
||||||
|
* Linear Rich Quoting
|
||||||
|
[\#1715](https://github.com/matrix-org/matrix-react-sdk/pull/1715)
|
||||||
|
* Fix CreateGroupDialog issues
|
||||||
|
[\#1714](https://github.com/matrix-org/matrix-react-sdk/pull/1714)
|
||||||
|
* Show a warning if the user attempts to leave a room that is invite only
|
||||||
|
[\#1713](https://github.com/matrix-org/matrix-react-sdk/pull/1713)
|
||||||
|
* Swap RoomList to react-beautiful-dnd
|
||||||
|
[\#1711](https://github.com/matrix-org/matrix-react-sdk/pull/1711)
|
||||||
|
* don't pass back {} when we have no `org.matrix.room.color_scheme`
|
||||||
|
[\#1710](https://github.com/matrix-org/matrix-react-sdk/pull/1710)
|
||||||
|
* Don't paginate whilst decrypting events
|
||||||
|
[\#1700](https://github.com/matrix-org/matrix-react-sdk/pull/1700)
|
||||||
|
* Fall back for missing i18n plurals
|
||||||
|
[\#1699](https://github.com/matrix-org/matrix-react-sdk/pull/1699)
|
||||||
|
* Fix group store redundant requests
|
||||||
|
[\#1709](https://github.com/matrix-org/matrix-react-sdk/pull/1709)
|
||||||
|
* Ignore remote echos caused by this client
|
||||||
|
[\#1708](https://github.com/matrix-org/matrix-react-sdk/pull/1708)
|
||||||
|
* Replace TagPanel react-dnd with react-beautiful-dnd
|
||||||
|
[\#1705](https://github.com/matrix-org/matrix-react-sdk/pull/1705)
|
||||||
|
* Only set selected tags state when updating rooms
|
||||||
|
[\#1704](https://github.com/matrix-org/matrix-react-sdk/pull/1704)
|
||||||
|
* Add formatFullDateNoTime to DateUtils and stop passing 12/24h to DateSep
|
||||||
|
[\#1702](https://github.com/matrix-org/matrix-react-sdk/pull/1702)
|
||||||
|
* Fix autofocus on QuestionDialog
|
||||||
|
[\#1698](https://github.com/matrix-org/matrix-react-sdk/pull/1698)
|
||||||
|
* Iterative fixes on Rich Quoting
|
||||||
|
[\#1697](https://github.com/matrix-org/matrix-react-sdk/pull/1697)
|
||||||
|
* Fix missing negation
|
||||||
|
[\#1696](https://github.com/matrix-org/matrix-react-sdk/pull/1696)
|
||||||
|
* Add Analytics Info and add Piwik to SdkConfig.DEFAULTS
|
||||||
|
[\#1625](https://github.com/matrix-org/matrix-react-sdk/pull/1625)
|
||||||
|
* Attempt to re-register for a scalar token if ours is invalid
|
||||||
|
[\#1668](https://github.com/matrix-org/matrix-react-sdk/pull/1668)
|
||||||
|
* Normalise dialogs
|
||||||
|
[\#1674](https://github.com/matrix-org/matrix-react-sdk/pull/1674)
|
||||||
|
* Add 'send without verifying' to status bar
|
||||||
|
[\#1695](https://github.com/matrix-org/matrix-react-sdk/pull/1695)
|
||||||
|
* Implement Rich Quoting/Replies
|
||||||
|
[\#1660](https://github.com/matrix-org/matrix-react-sdk/pull/1660)
|
||||||
|
* Revert "MD-escape URLs/alises/user IDs prior to parsing markdown"
|
||||||
|
[\#1694](https://github.com/matrix-org/matrix-react-sdk/pull/1694)
|
||||||
|
* Cache isConfCallRoom
|
||||||
|
[\#1693](https://github.com/matrix-org/matrix-react-sdk/pull/1693)
|
||||||
|
* Improve performance of tag panel selection (when tags are selected)
|
||||||
|
[\#1687](https://github.com/matrix-org/matrix-react-sdk/pull/1687)
|
||||||
|
* Hide status bar on visible->hidden transition
|
||||||
|
[\#1680](https://github.com/matrix-org/matrix-react-sdk/pull/1680)
|
||||||
|
* [revived] Singularise unsent message prompt, if applicable
|
||||||
|
[\#1692](https://github.com/matrix-org/matrix-react-sdk/pull/1692)
|
||||||
|
* small refactor && warn on self-demotion
|
||||||
|
[\#1683](https://github.com/matrix-org/matrix-react-sdk/pull/1683)
|
||||||
|
* Remove use of deprecated React.PropTypes
|
||||||
|
[\#1677](https://github.com/matrix-org/matrix-react-sdk/pull/1677)
|
||||||
|
* only save RelatedGroupSettings if it was modified. Otherwise perms issue
|
||||||
|
[\#1691](https://github.com/matrix-org/matrix-react-sdk/pull/1691)
|
||||||
|
* Fix a couple more issues with granular settings
|
||||||
|
[\#1675](https://github.com/matrix-org/matrix-react-sdk/pull/1675)
|
||||||
|
* Allow argument to op slashcommand to be negative as PLs can be -ve
|
||||||
|
[\#1673](https://github.com/matrix-org/matrix-react-sdk/pull/1673)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#1645](https://github.com/matrix-org/matrix-react-sdk/pull/1645)
|
||||||
|
* make RoomDetailRow reusable for the Room Directory
|
||||||
|
[\#1624](https://github.com/matrix-org/matrix-react-sdk/pull/1624)
|
||||||
|
* Prefetch group data for all joined groups when RoomList mounts
|
||||||
|
[\#1686](https://github.com/matrix-org/matrix-react-sdk/pull/1686)
|
||||||
|
* Remove unused selectedRoom prop
|
||||||
|
[\#1690](https://github.com/matrix-org/matrix-react-sdk/pull/1690)
|
||||||
|
* Fix shift and shift-ctrl click in TagPanel
|
||||||
|
[\#1684](https://github.com/matrix-org/matrix-react-sdk/pull/1684)
|
||||||
|
* skip direct chats which either you or the target have left
|
||||||
|
[\#1344](https://github.com/matrix-org/matrix-react-sdk/pull/1344)
|
||||||
|
* Make scroll on paste in RTE compatible with https://github.com/vector-im
|
||||||
|
/riot-web/pull/5900
|
||||||
|
[\#1682](https://github.com/matrix-org/matrix-react-sdk/pull/1682)
|
||||||
|
* Remove extra full stop
|
||||||
|
[\#1685](https://github.com/matrix-org/matrix-react-sdk/pull/1685)
|
||||||
|
* Dedupe requests to fetch group profile data
|
||||||
|
[\#1666](https://github.com/matrix-org/matrix-react-sdk/pull/1666)
|
||||||
|
* Get Group profile from TagTile instead of TagPanel
|
||||||
|
[\#1667](https://github.com/matrix-org/matrix-react-sdk/pull/1667)
|
||||||
|
* Fix leaking of GroupStore listeners in RoomList
|
||||||
|
[\#1664](https://github.com/matrix-org/matrix-react-sdk/pull/1664)
|
||||||
|
* Add option to also output untranslated string
|
||||||
|
[\#1658](https://github.com/matrix-org/matrix-react-sdk/pull/1658)
|
||||||
|
* Give the current theme to widgets and the integration manager
|
||||||
|
[\#1669](https://github.com/matrix-org/matrix-react-sdk/pull/1669)
|
||||||
|
* Fixes #1953 Allow multiple file uploads using drag & drop for RoomView
|
||||||
|
[\#1671](https://github.com/matrix-org/matrix-react-sdk/pull/1671)
|
||||||
|
* Fix issue with preview of phone number on register and waiting for sms code
|
||||||
|
confirmation code
|
||||||
|
[\#1670](https://github.com/matrix-org/matrix-react-sdk/pull/1670)
|
||||||
|
* Attempt to improve TagPanel performance
|
||||||
|
[\#1647](https://github.com/matrix-org/matrix-react-sdk/pull/1647)
|
||||||
|
* Fix one variant of a scroll jump that occurs when decrypting an m.text
|
||||||
|
[\#1656](https://github.com/matrix-org/matrix-react-sdk/pull/1656)
|
||||||
|
* Avoid NPEs by using ref method for collecting loggedInView in MatrixChat
|
||||||
|
[\#1665](https://github.com/matrix-org/matrix-react-sdk/pull/1665)
|
||||||
|
* DnD Ordered TagPanel
|
||||||
|
[\#1653](https://github.com/matrix-org/matrix-react-sdk/pull/1653)
|
||||||
|
* Update widget title on edit.
|
||||||
|
[\#1663](https://github.com/matrix-org/matrix-react-sdk/pull/1663)
|
||||||
|
* Set widget title
|
||||||
|
[\#1661](https://github.com/matrix-org/matrix-react-sdk/pull/1661)
|
||||||
|
* Display custom widget content titles
|
||||||
|
[\#1650](https://github.com/matrix-org/matrix-react-sdk/pull/1650)
|
||||||
|
* Add maximize / minimize apps drawer icons.
|
||||||
|
[\#1649](https://github.com/matrix-org/matrix-react-sdk/pull/1649)
|
||||||
|
* Warn when migrating e2e data to indexeddb
|
||||||
|
[\#1654](https://github.com/matrix-org/matrix-react-sdk/pull/1654)
|
||||||
|
* Don't Auto-show UnknownDeviceDialog
|
||||||
|
[\#1600](https://github.com/matrix-org/matrix-react-sdk/pull/1600)
|
||||||
|
* Remove logging.
|
||||||
|
[\#1655](https://github.com/matrix-org/matrix-react-sdk/pull/1655)
|
||||||
|
* Add messaging endpoint for room encryption status.
|
||||||
|
[\#1648](https://github.com/matrix-org/matrix-react-sdk/pull/1648)
|
||||||
|
* Add some missing translatable strings
|
||||||
|
[\#1588](https://github.com/matrix-org/matrix-react-sdk/pull/1588)
|
||||||
|
* Add widget -> riot postMessage API
|
||||||
|
[\#1640](https://github.com/matrix-org/matrix-react-sdk/pull/1640)
|
||||||
|
* Add some null checks
|
||||||
|
[\#1646](https://github.com/matrix-org/matrix-react-sdk/pull/1646)
|
||||||
|
* Implement shift-click and ctrl-click semantics for TP
|
||||||
|
[\#1641](https://github.com/matrix-org/matrix-react-sdk/pull/1641)
|
||||||
|
* Don't show group when clicking tag panel
|
||||||
|
[\#1642](https://github.com/matrix-org/matrix-react-sdk/pull/1642)
|
||||||
|
* Implement TagPanel (or LeftLeftPanel) for group filtering
|
||||||
|
[\#1639](https://github.com/matrix-org/matrix-react-sdk/pull/1639)
|
||||||
|
* Implement UI for using bulk device deletion API
|
||||||
|
[\#1638](https://github.com/matrix-org/matrix-react-sdk/pull/1638)
|
||||||
|
* Replace (IRC) with flair
|
||||||
|
[\#1637](https://github.com/matrix-org/matrix-react-sdk/pull/1637)
|
||||||
|
* Allow guests to view individual groups
|
||||||
|
[\#1635](https://github.com/matrix-org/matrix-react-sdk/pull/1635)
|
||||||
|
* Allow guest to see MyGroups, show ILAG when creating a group
|
||||||
|
[\#1636](https://github.com/matrix-org/matrix-react-sdk/pull/1636)
|
||||||
|
* Move group publication toggles to UserSettings
|
||||||
|
[\#1634](https://github.com/matrix-org/matrix-react-sdk/pull/1634)
|
||||||
|
* Pull the theme through the default process
|
||||||
|
[\#1617](https://github.com/matrix-org/matrix-react-sdk/pull/1617)
|
||||||
|
* Rebase ConfirmRedactDialog on QuestionDialog
|
||||||
|
[\#1630](https://github.com/matrix-org/matrix-react-sdk/pull/1630)
|
||||||
|
* Fix logging of missing substitution variables
|
||||||
|
[\#1629](https://github.com/matrix-org/matrix-react-sdk/pull/1629)
|
||||||
|
* Rename Related Groups to improve readability
|
||||||
|
[\#1632](https://github.com/matrix-org/matrix-react-sdk/pull/1632)
|
||||||
|
* Make PresenceLabel more easily translatable
|
||||||
|
[\#1616](https://github.com/matrix-org/matrix-react-sdk/pull/1616)
|
||||||
|
* Perform substitution on all parts, not just the last one
|
||||||
|
[\#1618](https://github.com/matrix-org/matrix-react-sdk/pull/1618)
|
||||||
|
* Send Access Token in Headers to help prevent it being spit out in errors
|
||||||
|
[\#1552](https://github.com/matrix-org/matrix-react-sdk/pull/1552)
|
||||||
|
* Add aria-labels to ActionButtons
|
||||||
|
[\#1628](https://github.com/matrix-org/matrix-react-sdk/pull/1628)
|
||||||
|
* MemberPresenceAvatar: fix null references
|
||||||
|
[\#1620](https://github.com/matrix-org/matrix-react-sdk/pull/1620)
|
||||||
|
* Disable presence controls if there's no presence
|
||||||
|
[\#1623](https://github.com/matrix-org/matrix-react-sdk/pull/1623)
|
||||||
|
* Fix GroupMemberList search for users without displayname
|
||||||
|
[\#1627](https://github.com/matrix-org/matrix-react-sdk/pull/1627)
|
||||||
|
* Remove redundant super class EventEmitter for FlairStore
|
||||||
|
[\#1626](https://github.com/matrix-org/matrix-react-sdk/pull/1626)
|
||||||
|
* Fix granular URL previews
|
||||||
|
[\#1622](https://github.com/matrix-org/matrix-react-sdk/pull/1622)
|
||||||
|
* Flairstore: Fix broken reference
|
||||||
|
[\#1619](https://github.com/matrix-org/matrix-react-sdk/pull/1619)
|
||||||
|
* Do something more sensible for sender profile name/aux opacity
|
||||||
|
[\#1615](https://github.com/matrix-org/matrix-react-sdk/pull/1615)
|
||||||
|
* Add eslint rule keyword-spacing
|
||||||
|
[\#1614](https://github.com/matrix-org/matrix-react-sdk/pull/1614)
|
||||||
|
* Fix various issues surrounding granular settings to date
|
||||||
|
[\#1613](https://github.com/matrix-org/matrix-react-sdk/pull/1613)
|
||||||
|
* differentiate between state events and message events
|
||||||
|
[\#1612](https://github.com/matrix-org/matrix-react-sdk/pull/1612)
|
||||||
|
* Refactor translations
|
||||||
|
[\#1608](https://github.com/matrix-org/matrix-react-sdk/pull/1608)
|
||||||
|
* Make TintableSvg links behave like normal image links
|
||||||
|
[\#1611](https://github.com/matrix-org/matrix-react-sdk/pull/1611)
|
||||||
|
* Fix linting errors.
|
||||||
|
[\#1610](https://github.com/matrix-org/matrix-react-sdk/pull/1610)
|
||||||
|
* Granular settings
|
||||||
|
[\#1516](https://github.com/matrix-org/matrix-react-sdk/pull/1516)
|
||||||
|
* Implement user-controlled presence
|
||||||
|
[\#1482](https://github.com/matrix-org/matrix-react-sdk/pull/1482)
|
||||||
|
* Edit widget icon styling
|
||||||
|
[\#1609](https://github.com/matrix-org/matrix-react-sdk/pull/1609)
|
||||||
|
* Attempt to improve textual power levels
|
||||||
|
[\#1607](https://github.com/matrix-org/matrix-react-sdk/pull/1607)
|
||||||
|
* Determine whether power level is custom once Roles have been determined
|
||||||
|
[\#1606](https://github.com/matrix-org/matrix-react-sdk/pull/1606)
|
||||||
|
* Status.im theme
|
||||||
|
[\#1605](https://github.com/matrix-org/matrix-react-sdk/pull/1605)
|
||||||
|
* Revert "Lowercase all usernames"
|
||||||
|
[\#1604](https://github.com/matrix-org/matrix-react-sdk/pull/1604)
|
||||||
|
|
||||||
|
Changes in [0.11.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.4) (2018-02-09)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.3...v0.11.4)
|
||||||
|
|
||||||
|
* Add isUrlPermitted function to sanity check URLs
|
||||||
|
|
||||||
Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
|
Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
|
||||||
|
|
12
package.json
12
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.11.3",
|
"version": "0.12.1",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.27.0",
|
"commonmark": "^0.28.1",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.0",
|
||||||
"draft-js": "^0.11.0-alpha",
|
"draft-js": "^0.11.0-alpha",
|
||||||
"draft-js-export-html": "^0.6.0",
|
"draft-js-export-html": "^0.6.0",
|
||||||
|
@ -65,20 +65,20 @@
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.5.6",
|
||||||
"flux": "2.1.1",
|
"flux": "2.1.1",
|
||||||
|
"focus-trap-react": "^3.0.5",
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.2.0",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "^8.9.1",
|
"highlight.js": "^8.9.1",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "0.9.2",
|
"matrix-js-sdk": "0.10.0",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"querystring": "^0.2.0",
|
"querystring": "^0.2.0",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dnd": "^2.1.4",
|
"react-beautiful-dnd": "^4.0.0",
|
||||||
"react-dnd-html5-backend": "^2.1.2",
|
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
"sanitize-html": "^1.14.1",
|
"sanitize-html": "^1.14.1",
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
"require-json": "0.0.1",
|
"require-json": "0.0.1",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
"source-map-loader": "^0.1.5",
|
"source-map-loader": "^0.2.3",
|
||||||
"walk": "^2.3.9",
|
"walk": "^2.3.9",
|
||||||
"webpack": "^1.12.14"
|
"webpack": "^1.12.14"
|
||||||
}
|
}
|
||||||
|
|
111
src/Analytics.js
111
src/Analytics.js
|
@ -14,25 +14,54 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentLanguage } from './languageHandler';
|
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig, { DEFAULTS } from './SdkConfig';
|
||||||
|
import Modal from './Modal';
|
||||||
|
import sdk from './index';
|
||||||
|
|
||||||
|
function getRedactedHash() {
|
||||||
|
return window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
||||||
|
}
|
||||||
|
|
||||||
function getRedactedUrl() {
|
function getRedactedUrl() {
|
||||||
const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
|
||||||
// hardcoded url to make piwik happy
|
// hardcoded url to make piwik happy
|
||||||
return 'https://riot.im/app/' + redactedHash;
|
return 'https://riot.im/app/' + getRedactedHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
const customVariables = {
|
const customVariables = {
|
||||||
'App Platform': 1,
|
'App Platform': {
|
||||||
'App Version': 2,
|
id: 1,
|
||||||
'User Type': 3,
|
expl: _td('The platform you\'re on'),
|
||||||
'Chosen Language': 4,
|
},
|
||||||
'Instance': 5,
|
'App Version': {
|
||||||
'RTE: Uses Richtext Mode': 6,
|
id: 2,
|
||||||
'Homeserver URL': 7,
|
expl: _td('The version of Riot.im'),
|
||||||
'Identity Server URL': 8,
|
},
|
||||||
|
'User Type': {
|
||||||
|
id: 3,
|
||||||
|
expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'),
|
||||||
|
},
|
||||||
|
'Chosen Language': {
|
||||||
|
id: 4,
|
||||||
|
expl: _td('Your language of choice'),
|
||||||
|
},
|
||||||
|
'Instance': {
|
||||||
|
id: 5,
|
||||||
|
expl: _td('Which officially provided instance you are using, if any'),
|
||||||
|
},
|
||||||
|
'RTE: Uses Richtext Mode': {
|
||||||
|
id: 6,
|
||||||
|
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
||||||
|
},
|
||||||
|
'Homeserver URL': {
|
||||||
|
id: 7,
|
||||||
|
expl: _td('Your homeserver\'s URL'),
|
||||||
|
},
|
||||||
|
'Identity Server URL': {
|
||||||
|
id: 8,
|
||||||
|
expl: _td('Your identity server\'s URL'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function whitelistRedact(whitelist, str) {
|
function whitelistRedact(whitelist, str) {
|
||||||
|
@ -40,9 +69,6 @@ function whitelistRedact(whitelist, str) {
|
||||||
return '<redacted>';
|
return '<redacted>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const whitelistedHSUrls = ["https://matrix.org"];
|
|
||||||
const whitelistedISUrls = ["https://vector.im"];
|
|
||||||
|
|
||||||
class Analytics {
|
class Analytics {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._paq = null;
|
this._paq = null;
|
||||||
|
@ -66,6 +92,10 @@ class Analytics {
|
||||||
*/
|
*/
|
||||||
disable() {
|
disable() {
|
||||||
this.trackEvent('Analytics', 'opt-out');
|
this.trackEvent('Analytics', 'opt-out');
|
||||||
|
// disableHeartBeatTimer is undocumented but exists in the piwik code
|
||||||
|
// the _paq.push method will result in an error being printed in the console
|
||||||
|
// if an unknown method signature is passed
|
||||||
|
this._paq.push(['disableHeartBeatTimer']);
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +147,10 @@ class Analytics {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackPageChange() {
|
trackPageChange(generationTimeMs) {
|
||||||
|
if (typeof generationTimeMs !== 'number') {
|
||||||
|
throw new Error('Analytics.trackPageChange: expected generationTimeMs to be a number');
|
||||||
|
}
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
if (this.firstPage) {
|
if (this.firstPage) {
|
||||||
// De-duplicate first page
|
// De-duplicate first page
|
||||||
|
@ -126,6 +159,7 @@ class Analytics {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
||||||
|
this._paq.push(['setGenerationTimeMs', generationTimeMs]);
|
||||||
this._paq.push(['trackPageView']);
|
this._paq.push(['trackPageView']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,11 +174,16 @@ class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setVisitVariable(key, value) {
|
_setVisitVariable(key, value) {
|
||||||
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
|
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
|
const config = SdkConfig.get();
|
||||||
|
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || DEFAULTS.piwik.whitelistedHSUrls;
|
||||||
|
const whitelistedISUrls = config.piwik.whitelistedISUrls || DEFAULTS.piwik.whitelistedISUrls;
|
||||||
|
|
||||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
|
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
|
||||||
|
@ -154,6 +193,44 @@ class Analytics {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetailsModal() {
|
||||||
|
const Tracker = window.Piwik.getAsyncTracker();
|
||||||
|
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
||||||
|
|
||||||
|
const resolution = `${window.screen.width}x${window.screen.height}`;
|
||||||
|
|
||||||
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
|
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
||||||
|
title: _t('Analytics'),
|
||||||
|
description: <div>
|
||||||
|
<div>
|
||||||
|
{ _t('The information being sent to us to help make Riot.im better includes:') }
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
{ rows.map((row) => <tr key={row[0]}>
|
||||||
|
<td>{ _t(customVariables[row[0]].expl) }</td>
|
||||||
|
<td><code>{ row[1] }</code></td>
|
||||||
|
</tr>) }
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
{ _t('We also record each page you use in the app (currently <CurrentPageHash>), your User Agent'
|
||||||
|
+ ' (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
CurrentPageHash: <code>{ getRedactedHash() }</code>,
|
||||||
|
CurrentUserAgent: <code>{ navigator.userAgent }</code>,
|
||||||
|
CurrentDeviceResolution: <code>{ resolution }</code>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ _t('Where this page includes identifiable information, such as a room, '
|
||||||
|
+ 'user or group ID, that data is removed before being sent to the server.') }
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.mxAnalytics) {
|
if (!global.mxAnalytics) {
|
||||||
|
|
|
@ -275,6 +275,13 @@ class ContentMessages {
|
||||||
this.nextId = 0;
|
this.nextId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
|
||||||
|
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||||
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendContentToRoom(file, roomId, matrixClient) {
|
sendContentToRoom(file, roomId, matrixClient) {
|
||||||
const content = {
|
const content = {
|
||||||
body: file.name || 'Attachment',
|
body: file.name || 'Attachment',
|
||||||
|
|
115
src/DateUtils.js
115
src/DateUtils.js
|
@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
function getDaysArray() {
|
function getDaysArray() {
|
||||||
|
@ -51,55 +50,89 @@ function pad(n) {
|
||||||
return (n < 10 ? '0' : '') + n;
|
return (n < 10 ? '0' : '') + n;
|
||||||
}
|
}
|
||||||
|
|
||||||
function twelveHourTime(date) {
|
function twelveHourTime(date, showSeconds=false) {
|
||||||
let hours = date.getHours() % 12;
|
let hours = date.getHours() % 12;
|
||||||
const minutes = pad(date.getMinutes());
|
const minutes = pad(date.getMinutes());
|
||||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||||
hours = hours ? hours : 12; // convert 0 -> 12
|
hours = hours ? hours : 12; // convert 0 -> 12
|
||||||
|
if (showSeconds) {
|
||||||
|
const seconds = pad(date.getSeconds());
|
||||||
|
return `${hours}:${minutes}:${seconds}${ampm}`;
|
||||||
|
}
|
||||||
return `${hours}:${minutes}${ampm}`;
|
return `${hours}:${minutes}${ampm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export function formatDate(date, showTwelveHour=false) {
|
||||||
formatDate: function(date, showTwelveHour=false) {
|
const now = new Date();
|
||||||
const now = new Date();
|
const days = getDaysArray();
|
||||||
const days = getDaysArray();
|
const months = getMonthsArray();
|
||||||
const months = getMonthsArray();
|
if (date.toDateString() === now.toDateString()) {
|
||||||
if (date.toDateString() === now.toDateString()) {
|
return formatTime(date, showTwelveHour);
|
||||||
return this.formatTime(date, showTwelveHour);
|
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||||
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
// TODO: use standard date localize function provided in counterpart
|
||||||
// TODO: use standard date localize function provided in counterpart
|
return _t('%(weekDayName)s %(time)s', {
|
||||||
return _t('%(weekDayName)s %(time)s', {
|
weekDayName: days[date.getDay()],
|
||||||
weekDayName: days[date.getDay()],
|
time: formatTime(date, showTwelveHour),
|
||||||
time: this.formatTime(date, showTwelveHour),
|
});
|
||||||
});
|
} else if (now.getFullYear() === date.getFullYear()) {
|
||||||
} else if (now.getFullYear() === date.getFullYear()) {
|
// TODO: use standard date localize function provided in counterpart
|
||||||
// TODO: use standard date localize function provided in counterpart
|
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
|
||||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
|
|
||||||
weekDayName: days[date.getDay()],
|
|
||||||
monthName: months[date.getMonth()],
|
|
||||||
day: date.getDate(),
|
|
||||||
time: this.formatTime(date, showTwelveHour),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.formatFullDate(date, showTwelveHour);
|
|
||||||
},
|
|
||||||
|
|
||||||
formatFullDate: function(date, showTwelveHour=false) {
|
|
||||||
const days = getDaysArray();
|
|
||||||
const months = getMonthsArray();
|
|
||||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
|
||||||
weekDayName: days[date.getDay()],
|
weekDayName: days[date.getDay()],
|
||||||
monthName: months[date.getMonth()],
|
monthName: months[date.getMonth()],
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
fullYear: date.getFullYear(),
|
time: formatTime(date, showTwelveHour),
|
||||||
time: this.formatTime(date, showTwelveHour),
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
return formatFullDate(date, showTwelveHour);
|
||||||
|
}
|
||||||
|
|
||||||
formatTime: function(date, showTwelveHour=false) {
|
export function formatFullDateNoTime(date) {
|
||||||
if (showTwelveHour) {
|
const days = getDaysArray();
|
||||||
return twelveHourTime(date);
|
const months = getMonthsArray();
|
||||||
}
|
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
weekDayName: days[date.getDay()],
|
||||||
},
|
monthName: months[date.getMonth()],
|
||||||
};
|
day: date.getDate(),
|
||||||
|
fullYear: date.getFullYear(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFullDate(date, showTwelveHour=false) {
|
||||||
|
const days = getDaysArray();
|
||||||
|
const months = getMonthsArray();
|
||||||
|
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
||||||
|
weekDayName: days[date.getDay()],
|
||||||
|
monthName: months[date.getMonth()],
|
||||||
|
day: date.getDate(),
|
||||||
|
fullYear: date.getFullYear(),
|
||||||
|
time: formatFullTime(date, showTwelveHour),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFullTime(date, showTwelveHour=false) {
|
||||||
|
if (showTwelveHour) {
|
||||||
|
return twelveHourTime(date, true);
|
||||||
|
}
|
||||||
|
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTime(date, showTwelveHour=false) {
|
||||||
|
if (showTwelveHour) {
|
||||||
|
return twelveHourTime(date);
|
||||||
|
}
|
||||||
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
const MILLIS_IN_DAY = 86400000;
|
||||||
|
export function wantsDateSeparator(prevEventDate, nextEventDate) {
|
||||||
|
if (!nextEventDate || !prevEventDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Return early for events that are > 24h apart
|
||||||
|
if (Math.abs(prevEventDate.getTime() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare weekdays
|
||||||
|
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||||
|
}
|
||||||
|
|
201
src/FromWidgetPostMessageApi.js
Normal file
201
src/FromWidgetPostMessageApi.js
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
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 URL from 'url';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
import IntegrationManager from './IntegrationManager';
|
||||||
|
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||||
|
|
||||||
|
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
||||||
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
'0.0.1',
|
||||||
|
];
|
||||||
|
const INBOUND_API_NAME = 'fromWidget';
|
||||||
|
|
||||||
|
// Listen for and handle incomming requests using the 'fromWidget' postMessage
|
||||||
|
// API and initiate responses
|
||||||
|
export default class FromWidgetPostMessageApi {
|
||||||
|
constructor() {
|
||||||
|
this.widgetMessagingEndpoints = [];
|
||||||
|
|
||||||
|
this.start = this.start.bind(this);
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
this.onPostMessage = this.onPostMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
window.addEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
window.removeEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a widget endpoint for trusted postMessage communication
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
|
*/
|
||||||
|
addEndpoint(widgetId, endpointUrl) {
|
||||||
|
const u = URL.parse(endpointUrl);
|
||||||
|
if (!u || !u.protocol || !u.host) {
|
||||||
|
console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
|
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
||||||
|
if (this.widgetMessagingEndpoints.some(function(ep) {
|
||||||
|
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
||||||
|
})) {
|
||||||
|
// Message endpoint already registered
|
||||||
|
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.warn(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
||||||
|
this.widgetMessagingEndpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-register a widget endpoint from trusted communication sources
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
|
* @return {boolean} True if endpoint was successfully removed
|
||||||
|
*/
|
||||||
|
removeEndpoint(widgetId, endpointUrl) {
|
||||||
|
const u = URL.parse(endpointUrl);
|
||||||
|
if (!u || !u.protocol || !u.host) {
|
||||||
|
console.warn('Remove widget messaging endpoint - Invalid origin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
|
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
||||||
|
const length = this.widgetMessagingEndpoints.length;
|
||||||
|
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints.
|
||||||
|
filter(function(endpoint) {
|
||||||
|
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
||||||
|
});
|
||||||
|
return (length > this.widgetMessagingEndpoints.length);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle widget postMessage events
|
||||||
|
* Messages are only handled where a valid, registered messaging endpoints
|
||||||
|
* @param {Event} event Event to handle
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
onPostMessage(event) {
|
||||||
|
if (!event.origin) { // Handle chrome
|
||||||
|
event.origin = event.originalEvent.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event origin is empty string if undefined
|
||||||
|
if (
|
||||||
|
event.origin.length === 0 ||
|
||||||
|
!this.trustedEndpoint(event.origin) ||
|
||||||
|
event.data.api !== INBOUND_API_NAME ||
|
||||||
|
!event.data.widgetId
|
||||||
|
) {
|
||||||
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = event.data.action;
|
||||||
|
const widgetId = event.data.widgetId;
|
||||||
|
if (action === 'content_loaded') {
|
||||||
|
console.warn('Widget reported content loaded for', widgetId);
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'widget_content_loaded',
|
||||||
|
widgetId: widgetId,
|
||||||
|
});
|
||||||
|
this.sendResponse(event, {success: true});
|
||||||
|
} else if (action === 'supported_api_versions') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: INBOUND_API_NAME,
|
||||||
|
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
||||||
|
});
|
||||||
|
} else if (action === 'api_version') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: INBOUND_API_NAME,
|
||||||
|
version: WIDGET_API_VERSION,
|
||||||
|
});
|
||||||
|
} else if (action === 'm.sticker') {
|
||||||
|
// console.warn('Got sticker message from widget', widgetId);
|
||||||
|
dis.dispatch({action: 'm.sticker', data: event.data.widgetData, widgetId: event.data.widgetId});
|
||||||
|
} else if (action === 'integration_manager_open') {
|
||||||
|
// Close the stickerpicker
|
||||||
|
dis.dispatch({action: 'stickerpicker_close'});
|
||||||
|
// Open the integration manager
|
||||||
|
const data = event.data.widgetData;
|
||||||
|
const integType = (data && data.integType) ? data.integType : null;
|
||||||
|
const integId = (data && data.integId) ? data.integId : null;
|
||||||
|
IntegrationManager.open(integType, integId);
|
||||||
|
} else {
|
||||||
|
console.warn('Widget postMessage event unhandled');
|
||||||
|
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if message origin is registered as trusted
|
||||||
|
* @param {string} origin PostMessage origin to check
|
||||||
|
* @return {boolean} True if trusted
|
||||||
|
*/
|
||||||
|
trustedEndpoint(origin) {
|
||||||
|
if (!origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.widgetMessagingEndpoints.some((endpoint) => {
|
||||||
|
// TODO / FIXME -- Should this also check the widgetId?
|
||||||
|
return endpoint.endpointUrl === origin;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a postmessage response to a postMessage request
|
||||||
|
* @param {Event} event The original postMessage request event
|
||||||
|
* @param {Object} res Response data
|
||||||
|
*/
|
||||||
|
sendResponse(event, res) {
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = res;
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an error response to a postMessage request
|
||||||
|
* @param {Event} event The original postMessage request event
|
||||||
|
* @param {string} msg Error message
|
||||||
|
* @param {Error} nestedError Nested error event (optional)
|
||||||
|
*/
|
||||||
|
sendError(event, msg, nestedError) {
|
||||||
|
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = {
|
||||||
|
error: {
|
||||||
|
message: msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (nestedError) {
|
||||||
|
data.response.error._error = nestedError;
|
||||||
|
}
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,28 +22,30 @@ import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import GroupStoreCache from './stores/GroupStoreCache';
|
import GroupStoreCache from './stores/GroupStoreCache';
|
||||||
|
|
||||||
export function showGroupInviteDialog(groupId) {
|
export function showGroupInviteDialog(groupId) {
|
||||||
const description = <div>
|
return new Promise((resolve, reject) => {
|
||||||
<div>{ _t("Who would you like to add to this community?") }</div>
|
const description = <div>
|
||||||
<div className="warning">
|
<div>{ _t("Who would you like to add to this community?") }</div>
|
||||||
{ _t(
|
<div className="warning">
|
||||||
"Warning: any person you add to a community will be publicly "+
|
{ _t(
|
||||||
"visible to anyone who knows the community ID",
|
"Warning: any person you add to a community will be publicly "+
|
||||||
) }
|
"visible to anyone who knows the community ID",
|
||||||
</div>
|
) }
|
||||||
</div>;
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
||||||
title: _t("Invite new community members"),
|
title: _t("Invite new community members"),
|
||||||
description: description,
|
description: description,
|
||||||
placeholder: _t("Name or matrix ID"),
|
placeholder: _t("Name or matrix ID"),
|
||||||
button: _t("Invite to Community"),
|
button: _t("Invite to Community"),
|
||||||
validAddressTypes: ['mx-user-id'],
|
validAddressTypes: ['mx-user-id'],
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
|
|
||||||
_onGroupInviteFinished(groupId, addrs);
|
_onGroupInviteFinished(groupId, addrs).then(resolve, reject);
|
||||||
},
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +89,7 @@ function _onGroupInviteFinished(groupId, addrs) {
|
||||||
|
|
||||||
const addrTexts = addrs.map((addr) => addr.address);
|
const addrTexts = addrs.map((addr) => addr.address);
|
||||||
|
|
||||||
multiInviter.invite(addrTexts).then((completionStates) => {
|
return multiInviter.invite(addrTexts).then((completionStates) => {
|
||||||
// Show user any errors
|
// Show user any errors
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
for (const addr of Object.keys(completionStates)) {
|
for (const addr of Object.keys(completionStates)) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -25,6 +25,7 @@ import escape from 'lodash/escape';
|
||||||
import emojione from 'emojione';
|
import emojione from 'emojione';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
emojione.imagePathSVG = 'emojione/svg/';
|
emojione.imagePathSVG = 'emojione/svg/';
|
||||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||||
|
@ -44,6 +45,8 @@ const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
|
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Return true if the given string contains emoji
|
* Return true if the given string contains emoji
|
||||||
* Uses a much, much simpler regex than emojione's so will give false
|
* Uses a much, much simpler regex than emojione's so will give false
|
||||||
|
@ -152,6 +155,25 @@ export function sanitizedHtmlNode(insaneHtml) {
|
||||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if a URL from an untrusted source may be safely put into the DOM
|
||||||
|
* The biggest threat here is javascript: URIs.
|
||||||
|
* Note that the HTML sanitiser library has its own internal logic for
|
||||||
|
* doing this, to which we pass the same list of schemes. This is used in
|
||||||
|
* other places we need to sanitise URLs.
|
||||||
|
* @return true if permitted, otherwise false
|
||||||
|
*/
|
||||||
|
export function isUrlPermitted(inputUrl) {
|
||||||
|
try {
|
||||||
|
const parsed = url.parse(inputUrl);
|
||||||
|
if (!parsed.protocol) return false;
|
||||||
|
// URL parser protocol includes the trailing colon
|
||||||
|
return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1));
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sanitizeHtmlParams = {
|
const sanitizeHtmlParams = {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
|
@ -172,7 +194,7 @@ const sanitizeHtmlParams = {
|
||||||
// Lots of these won't come up by default because we don't allow them
|
// Lots of these won't come up by default because we don't allow them
|
||||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||||
// URL schemes we permit
|
// URL schemes we permit
|
||||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
|
allowedSchemes: PERMITTED_URL_SCHEMES,
|
||||||
|
|
||||||
allowProtocolRelative: false,
|
allowProtocolRelative: false,
|
||||||
|
|
||||||
|
@ -388,8 +410,7 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
||||||
*/
|
*/
|
||||||
export function bodyToHtml(content, highlights, opts={}) {
|
export function bodyToHtml(content, highlights, opts={}) {
|
||||||
const isHtml = (content.format === "org.matrix.custom.html");
|
let isHtml = (content.format === "org.matrix.custom.html");
|
||||||
const body = isHtml ? content.formatted_body : escape(content.body);
|
|
||||||
|
|
||||||
let bodyHasEmoji = false;
|
let bodyHasEmoji = false;
|
||||||
|
|
||||||
|
@ -409,9 +430,27 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
|
||||||
bodyHasEmoji = containsEmoji(body);
|
bodyHasEmoji = containsEmoji(isHtml ? content.formatted_body : content.body);
|
||||||
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
|
|
||||||
|
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||||
|
if (isHtml) {
|
||||||
|
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||||
|
} else {
|
||||||
|
// ... or if there are emoji, which we insert as HTML alongside the
|
||||||
|
// escaped plaintext body.
|
||||||
|
if (bodyHasEmoji) {
|
||||||
|
isHtml = true;
|
||||||
|
safeBody = sanitizeHtml(escape(content.body), sanitizeHtmlParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An HTML message with emoji
|
||||||
|
// or a plaintext message with emoji that was escaped and sanitized into
|
||||||
|
// HTML.
|
||||||
|
if (bodyHasEmoji) {
|
||||||
|
safeBody = unicodeToImage(safeBody);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
delete sanitizeHtmlParams.textFilter;
|
delete sanitizeHtmlParams.textFilter;
|
||||||
}
|
}
|
||||||
|
@ -429,7 +468,10 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
'mx_EventTile_bigEmoji': emojiBody,
|
'mx_EventTile_bigEmoji': emojiBody,
|
||||||
'markdown-body': isHtml,
|
'markdown-body': isHtml,
|
||||||
});
|
});
|
||||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
|
||||||
|
return isHtml ?
|
||||||
|
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||||
|
<span className={className} dir="auto">{ content.body }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emojifyText(text) {
|
export function emojifyText(text) {
|
||||||
|
|
73
src/IntegrationManager.js
Normal file
73
src/IntegrationManager.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 Modal from './Modal';
|
||||||
|
import sdk from './index';
|
||||||
|
import SdkConfig from './SdkConfig';
|
||||||
|
import ScalarMessaging from './ScalarMessaging';
|
||||||
|
import ScalarAuthClient from './ScalarAuthClient';
|
||||||
|
import RoomViewStore from './stores/RoomViewStore';
|
||||||
|
|
||||||
|
if (!global.mxIntegrationManager) {
|
||||||
|
global.mxIntegrationManager = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IntegrationManager {
|
||||||
|
static _init() {
|
||||||
|
if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) {
|
||||||
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
|
ScalarMessaging.startListening();
|
||||||
|
global.mxIntegrationManager.client = new ScalarAuthClient();
|
||||||
|
|
||||||
|
return global.mxIntegrationManager.client.connect().then(() => {
|
||||||
|
global.mxIntegrationManager.connected = true;
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error("Failed to connect to integrations server", e);
|
||||||
|
global.mxIntegrationManager.error = e;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Invalid integration manager config', SdkConfig.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the integrations manager on the stickers integration page
|
||||||
|
* @param {string} integName integration / widget type
|
||||||
|
* @param {string} integId integration / widget ID
|
||||||
|
* @param {function} onFinished Callback to invoke on integration manager close
|
||||||
|
*/
|
||||||
|
static async open(integName, integId, onFinished) {
|
||||||
|
await IntegrationManager._init();
|
||||||
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
|
if (global.mxIntegrationManager.error ||
|
||||||
|
!(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) {
|
||||||
|
console.error("Scalar error", global.mxIntegrationManager);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const integType = 'type_' + integName;
|
||||||
|
const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ?
|
||||||
|
global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom(
|
||||||
|
{roomId: RoomViewStore.getRoomId()},
|
||||||
|
integType,
|
||||||
|
integId,
|
||||||
|
) :
|
||||||
|
null;
|
||||||
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
|
src: src,
|
||||||
|
onFinished: onFinished,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,3 +68,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) {
|
||||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) {
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
|
if (isMac) {
|
||||||
|
return ev.metaKey && !ev.altKey && !ev.ctrlKey;
|
||||||
|
} else {
|
||||||
|
return ev.ctrlKey && !ev.altKey && !ev.metaKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -362,7 +362,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
||||||
});
|
});
|
||||||
|
|
||||||
startMatrixClient();
|
await startMatrixClient();
|
||||||
return MatrixClientPeg.get();
|
return MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,7 +423,7 @@ export function logout() {
|
||||||
* Starts the matrix client and all other react-sdk services that
|
* Starts the matrix client and all other react-sdk services that
|
||||||
* listen for events while a session is logged in.
|
* listen for events while a session is logged in.
|
||||||
*/
|
*/
|
||||||
function startMatrixClient() {
|
async function startMatrixClient() {
|
||||||
console.log(`Lifecycle: Starting MatrixClient`);
|
console.log(`Lifecycle: Starting MatrixClient`);
|
||||||
|
|
||||||
// dispatch this before starting the matrix client: it's used
|
// dispatch this before starting the matrix client: it's used
|
||||||
|
@ -437,7 +437,7 @@ function startMatrixClient() {
|
||||||
Presence.start();
|
Presence.start();
|
||||||
DMRoomMap.makeShared().start();
|
DMRoomMap.makeShared().start();
|
||||||
|
|
||||||
MatrixClientPeg.start();
|
await MatrixClientPeg.start();
|
||||||
|
|
||||||
// dispatch that we finished starting up to wire up any other bits
|
// dispatch that we finished starting up to wire up any other bits
|
||||||
// of the matrix client that cannot be set prior to starting up.
|
// of the matrix client that cannot be set prior to starting up.
|
||||||
|
|
|
@ -55,25 +55,6 @@ function is_multi_line(node) {
|
||||||
return par.firstChild != par.lastChild;
|
return par.firstChild != par.lastChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
import linkifyMatrix from './linkify-matrix';
|
|
||||||
import * as linkify from 'linkifyjs';
|
|
||||||
linkifyMatrix(linkify);
|
|
||||||
|
|
||||||
// Thieved from draft-js-export-markdown
|
|
||||||
function escapeMarkdown(s) {
|
|
||||||
return s.replace(/[*_`]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace URLs, room aliases and user IDs with md-escaped URLs
|
|
||||||
function linkifyMarkdown(s) {
|
|
||||||
const links = linkify.find(s);
|
|
||||||
links.forEach((l) => {
|
|
||||||
// This may replace several instances of `l.value` at once, but that's OK
|
|
||||||
s = s.replace(l.value, escapeMarkdown(l.value));
|
|
||||||
});
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that wraps commonmark, adding the ability to see whether
|
* Class that wraps commonmark, adding the ability to see whether
|
||||||
* a given message actually uses any markdown syntax or whether
|
* a given message actually uses any markdown syntax or whether
|
||||||
|
@ -81,7 +62,7 @@ function linkifyMarkdown(s) {
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = linkifyMarkdown(input);
|
this.input = input;
|
||||||
|
|
||||||
const parser = new commonmark.Parser();
|
const parser = new commonmark.Parser();
|
||||||
this.parsed = parser.parse(this.input);
|
this.parsed = parser.parse(this.input);
|
||||||
|
|
|
@ -174,4 +174,4 @@ class MatrixClientPeg {
|
||||||
if (!global.mxMatrixClientPeg) {
|
if (!global.mxMatrixClientPeg) {
|
||||||
global.mxMatrixClientPeg = new MatrixClientPeg();
|
global.mxMatrixClientPeg = new MatrixClientPeg();
|
||||||
}
|
}
|
||||||
module.exports = global.mxMatrixClientPeg;
|
export default global.mxMatrixClientPeg;
|
||||||
|
|
16
src/Modal.js
16
src/Modal.js
|
@ -19,8 +19,10 @@ limitations under the License.
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
|
|
||||||
|
@ -33,7 +35,7 @@ const AsyncWrapper = React.createClass({
|
||||||
/** A function which takes a 'callback' argument which it will call
|
/** A function which takes a 'callback' argument which it will call
|
||||||
* with the real component once it loads.
|
* with the real component once it loads.
|
||||||
*/
|
*/
|
||||||
loader: React.PropTypes.func.isRequired,
|
loader: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -187,10 +189,22 @@ class ModalManager {
|
||||||
|
|
||||||
_reRender() {
|
_reRender() {
|
||||||
if (this._modals.length == 0) {
|
if (this._modals.length == 0) {
|
||||||
|
// If there is no modal to render, make all of Riot available
|
||||||
|
// to screen reader users again
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'aria_unhide_main_app',
|
||||||
|
});
|
||||||
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the content outside the modal to screen reader users
|
||||||
|
// so they won't be able to navigate into it and act on it using
|
||||||
|
// screen reader specific features
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'aria_hide_main_app',
|
||||||
|
});
|
||||||
|
|
||||||
const modal = this._modals[0];
|
const modal = this._modals[0];
|
||||||
const dialog = (
|
const dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
||||||
|
|
|
@ -135,6 +135,10 @@ const Notifier = {
|
||||||
const plaf = PlatformPeg.get();
|
const plaf = PlatformPeg.get();
|
||||||
if (!plaf) return;
|
if (!plaf) return;
|
||||||
|
|
||||||
|
// Dev note: We don't set the "notificationsEnabled" setting to true here because it is a
|
||||||
|
// calculated value. It is determined based upon whether or not the master rule is enabled
|
||||||
|
// and other flags. Setting it here would cause a circular reference.
|
||||||
|
|
||||||
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
|
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
|
||||||
|
|
||||||
// make sure that we persist the current setting audio_enabled setting
|
// make sure that we persist the current setting audio_enabled setting
|
||||||
|
@ -168,7 +172,7 @@ const Notifier = {
|
||||||
});
|
});
|
||||||
// clear the notifications_hidden flag, so that if notifications are
|
// clear the notifications_hidden flag, so that if notifications are
|
||||||
// disabled again in the future, we will show the banner again.
|
// disabled again in the future, we will show the banner again.
|
||||||
this.setToolbarHidden(false);
|
this.setToolbarHidden(true);
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
|
@ -252,6 +256,10 @@ const Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onEventDecrypted: function(ev) {
|
onEventDecrypted: function(ev) {
|
||||||
|
// 'decrypted' means the decryption process has finished: it may have failed,
|
||||||
|
// in which case it might decrypt soon if the keys arrive
|
||||||
|
if (ev.isDecryptionFailure()) return;
|
||||||
|
|
||||||
const idx = this.pendingEncryptedEventIds.indexOf(ev.getId());
|
const idx = this.pendingEncryptedEventIds.indexOf(ev.getId());
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -31,7 +32,7 @@ class Presence {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
if (undefined === this.state) {
|
if (undefined === this.state) {
|
||||||
this._resetTimer();
|
this._resetTimer();
|
||||||
this.dispatcherRef = dis.register(this._onUserActivity.bind(this));
|
this.dispatcherRef = dis.register(this._onAction.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,8 +96,10 @@ class Presence {
|
||||||
this.setState("unavailable");
|
this.setState("unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserActivity() {
|
_onAction(payload) {
|
||||||
this._resetTimer();
|
if (payload.action === "user_activity") {
|
||||||
|
this._resetTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -85,9 +85,7 @@ function _onStartChatFinished(shouldInvite, addrs) {
|
||||||
if (rooms.length > 0) {
|
if (rooms.length > 0) {
|
||||||
// A Direct Message room already exists for this user, so select a
|
// A Direct Message room already exists for this user, so select a
|
||||||
// room from a list that is similar to the one in MemberInfo panel
|
// room from a list that is similar to the one in MemberInfo panel
|
||||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
const ChatCreateOrReuseDialog = sdk.getComponent("views.dialogs.ChatCreateOrReuseDialog");
|
||||||
"views.dialogs.ChatCreateOrReuseDialog",
|
|
||||||
);
|
|
||||||
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
|
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
|
||||||
userId: addrTexts[0],
|
userId: addrTexts[0],
|
||||||
onNewDMClick: () => {
|
onNewDMClick: () => {
|
||||||
|
@ -115,6 +113,15 @@ function _onStartChatFinished(shouldInvite, addrs) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (addrTexts.length === 1) {
|
||||||
|
// Start a new DM chat
|
||||||
|
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to invite user"),
|
||||||
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Start multi user chat
|
// Start multi user chat
|
||||||
let room;
|
let room;
|
||||||
|
|
|
@ -34,7 +34,14 @@ export function getRoomNotifsState(roomId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// for everything else, look at the room rule.
|
// for everything else, look at the room rule.
|
||||||
const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
|
let roomRule = null;
|
||||||
|
try {
|
||||||
|
roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
|
||||||
|
} catch (err) {
|
||||||
|
// Possible that the client doesn't have pushRules yet. If so, it
|
||||||
|
// hasn't started eiher, so indicate that this room is not notifying.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: We have to assume the default is to notify for all messages
|
// XXX: We have to assume the default is to notify for all messages
|
||||||
// (in particular this will be 'wrong' for one to one rooms because
|
// (in particular this will be 'wrong' for one to one rooms because
|
||||||
|
@ -130,6 +137,11 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOverrideMuteRule(roomId) {
|
function findOverrideMuteRule(roomId) {
|
||||||
|
if (!MatrixClientPeg.get().pushRules ||
|
||||||
|
!MatrixClientPeg.get().pushRules['global'] ||
|
||||||
|
!MatrixClientPeg.get().pushRules['global'].override) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
|
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
|
||||||
if (isRuleForRoom(roomId, rule)) {
|
if (isRuleForRoom(roomId, rule)) {
|
||||||
if (isMuteRule(rule) && rule.enabled) {
|
if (isMuteRule(rule) && rule.enabled) {
|
||||||
|
|
22
src/Rooms.js
22
src/Rooms.js
|
@ -43,7 +43,7 @@ export function getOnlyOtherMember(room, me) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isConfCallRoom(room, me, conferenceHandler) {
|
function _isConfCallRoom(room, me, conferenceHandler) {
|
||||||
if (!conferenceHandler) return false;
|
if (!conferenceHandler) return false;
|
||||||
|
|
||||||
if (me.membership != "join") {
|
if (me.membership != "join") {
|
||||||
|
@ -58,6 +58,26 @@ export function isConfCallRoom(room, me, conferenceHandler) {
|
||||||
if (conferenceHandler.isConferenceUser(otherMember.userId)) {
|
if (conferenceHandler.isConferenceUser(otherMember.userId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache whether a room is a conference call. Assumes that rooms will always
|
||||||
|
// either will or will not be a conference call room.
|
||||||
|
const isConfCallRoomCache = {
|
||||||
|
// $roomId: bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isConfCallRoom(room, me, conferenceHandler) {
|
||||||
|
if (isConfCallRoomCache[room.roomId] !== undefined) {
|
||||||
|
return isConfCallRoomCache[room.roomId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = _isConfCallRoom(room, me, conferenceHandler);
|
||||||
|
|
||||||
|
isConfCallRoomCache[room.roomId] = result;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function looksLikeDirectMessageRoom(room, me) {
|
export function looksLikeDirectMessageRoom(room, me) {
|
||||||
|
|
|
@ -39,11 +39,53 @@ class ScalarAuthClient {
|
||||||
|
|
||||||
// Returns a scalar_token string
|
// Returns a scalar_token string
|
||||||
getScalarToken() {
|
getScalarToken() {
|
||||||
const tok = window.localStorage.getItem("mx_scalar_token");
|
const token = window.localStorage.getItem("mx_scalar_token");
|
||||||
if (tok) return Promise.resolve(tok);
|
|
||||||
|
|
||||||
// No saved token, so do the dance to get one. First, we
|
if (!token) {
|
||||||
// need an openid bearer token from the HS.
|
return this.registerForToken();
|
||||||
|
} else {
|
||||||
|
return this.validateToken(token).then(userId => {
|
||||||
|
const me = MatrixClientPeg.get().getUserId();
|
||||||
|
if (userId !== me) {
|
||||||
|
throw new Error("Scalar token is owned by someone else: " + me);
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
// 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 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 defer.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerForToken() {
|
||||||
|
// Get openid bearer token from the HS as the first part of our dance
|
||||||
return MatrixClientPeg.get().getOpenIdToken().then((token_object) => {
|
return MatrixClientPeg.get().getOpenIdToken().then((token_object) => {
|
||||||
// Now we can send that to scalar and exchange it for a scalar token
|
// Now we can send that to scalar and exchange it for a scalar token
|
||||||
return this.exchangeForScalarToken(token_object);
|
return this.exchangeForScalarToken(token_object);
|
||||||
|
@ -106,10 +148,48 @@ class ScalarAuthClient {
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
/**
|
||||||
|
* Mark all assets associated with the specified widget as "disabled" in the
|
||||||
|
* integration manager database.
|
||||||
|
* This can be useful to temporarily prevent purchased assets from being displayed.
|
||||||
|
* @param {string} widgetType [description]
|
||||||
|
* @param {string} widgetId [description]
|
||||||
|
* @return {Promise} Resolves on completion
|
||||||
|
*/
|
||||||
|
disableWidgetAssets(widgetType, widgetId) {
|
||||||
|
let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state';
|
||||||
|
url = this.getStarterLink(url);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request({
|
||||||
|
method: 'GET',
|
||||||
|
uri: url,
|
||||||
|
json: true,
|
||||||
|
qs: {
|
||||||
|
'widget_type': widgetType,
|
||||||
|
'widget_id': widgetId,
|
||||||
|
'state': 'disable',
|
||||||
|
},
|
||||||
|
}, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else if (response.statusCode / 100 !== 2) {
|
||||||
|
reject({statusCode: response.statusCode});
|
||||||
|
} else if (!body) {
|
||||||
|
reject(new Error("Failed to set widget assets state"));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getScalarInterfaceUrlForRoom(room, screen, id) {
|
||||||
|
const roomId = room.roomId;
|
||||||
|
const roomName = room.name;
|
||||||
let url = SdkConfig.get().integrations_ui_url;
|
let url = SdkConfig.get().integrations_ui_url;
|
||||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||||
url += "&room_id=" + encodeURIComponent(roomId);
|
url += "&room_id=" + encodeURIComponent(roomId);
|
||||||
|
url += "&room_name=" + encodeURIComponent(roomName);
|
||||||
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
|
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
|
||||||
if (id) {
|
if (id) {
|
||||||
url += '&integ_id=' + encodeURIComponent(id);
|
url += '&integ_id=' + encodeURIComponent(id);
|
||||||
|
|
|
@ -235,6 +235,7 @@ const SdkConfig = require('./SdkConfig');
|
||||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
||||||
const dis = require("./dispatcher");
|
const dis = require("./dispatcher");
|
||||||
|
const Widgets = require('./utils/widgets');
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
function sendResponse(event, res) {
|
function sendResponse(event, res) {
|
||||||
|
@ -291,6 +292,7 @@ function setWidget(event, roomId) {
|
||||||
const widgetUrl = event.data.url;
|
const widgetUrl = event.data.url;
|
||||||
const widgetName = event.data.name; // optional
|
const widgetName = event.data.name; // optional
|
||||||
const widgetData = event.data.data; // optional
|
const widgetData = event.data.data; // optional
|
||||||
|
const userWidget = event.data.userWidget;
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -330,17 +332,54 @@ function setWidget(event, roomId) {
|
||||||
name: widgetName,
|
name: widgetName,
|
||||||
data: widgetData,
|
data: widgetData,
|
||||||
};
|
};
|
||||||
if (widgetUrl === null) { // widget is being deleted
|
|
||||||
content = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
if (userWidget) {
|
||||||
sendResponse(event, {
|
const client = MatrixClientPeg.get();
|
||||||
success: true,
|
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(),
|
||||||
|
stateKey: widgetId,
|
||||||
|
type: 'm.widget',
|
||||||
|
id: widgetId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
client.setAccountData('m.widgets', userWidgets).then(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
dis.dispatch({ action: "user_widget_updated" });
|
||||||
});
|
});
|
||||||
}, (err) => {
|
} else { // Room widget
|
||||||
sendError(event, _t('Failed to send request.'), err);
|
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(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
sendError(event, _t('Failed to send request.'), err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWidgets(event, roomId) {
|
function getWidgets(event, roomId) {
|
||||||
|
@ -349,19 +388,30 @@ function getWidgets(event, roomId) {
|
||||||
sendError(event, _t('You need to be logged in.'));
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
let widgetStateEvents = [];
|
||||||
if (!room) {
|
|
||||||
sendError(event, _t('This room is not recognised.'));
|
if (roomId) {
|
||||||
return;
|
const room = client.getRoom(roomId);
|
||||||
}
|
if (!room) {
|
||||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
sendError(event, _t('This room is not recognised.'));
|
||||||
// Only return widgets which have required fields
|
return;
|
||||||
const widgetStateEvents = [];
|
|
||||||
stateEvents.forEach((ev) => {
|
|
||||||
if (ev.getContent().type && ev.getContent().url) {
|
|
||||||
widgetStateEvents.push(ev.event); // return the raw event
|
|
||||||
}
|
}
|
||||||
});
|
// 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user widgets (not linked to a specific room)
|
||||||
|
const userWidgets = Widgets.getUserWidgetsArray();
|
||||||
|
widgetStateEvents = widgetStateEvents.concat(userWidgets);
|
||||||
|
|
||||||
sendResponse(event, widgetStateEvents);
|
sendResponse(event, widgetStateEvents);
|
||||||
}
|
}
|
||||||
|
@ -563,7 +613,7 @@ const onMessage = function(event) {
|
||||||
const url = SdkConfig.get().integrations_ui_url;
|
const url = SdkConfig.get().integrations_ui_url;
|
||||||
if (
|
if (
|
||||||
event.origin.length === 0 ||
|
event.origin.length === 0 ||
|
||||||
!url.startsWith(event.origin) ||
|
!url.startsWith(event.origin + '/') ||
|
||||||
!event.data.action ||
|
!event.data.action ||
|
||||||
event.data.api // Ignore messages with specific API set
|
event.data.api // Ignore messages with specific API set
|
||||||
) {
|
) {
|
||||||
|
@ -578,9 +628,22 @@ const onMessage = function(event) {
|
||||||
|
|
||||||
const roomId = event.data.room_id;
|
const roomId = event.data.room_id;
|
||||||
const userId = event.data.user_id;
|
const userId = event.data.user_id;
|
||||||
|
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
sendError(event, _t('Missing room_id in request'));
|
// These APIs don't require roomId
|
||||||
return;
|
// Get and set user widgets (not associated with a specific room)
|
||||||
|
// If roomId is specified, it must be validated, so room-based widgets agreed
|
||||||
|
// handled further down.
|
||||||
|
if (event.data.action === "get_widgets") {
|
||||||
|
getWidgets(event, null);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === "set_widget") {
|
||||||
|
setWidget(event, null);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
sendError(event, _t('Missing room_id in request'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let promise = Promise.resolve(currentRoomId);
|
let promise = Promise.resolve(currentRoomId);
|
||||||
if (!currentRoomId) {
|
if (!currentRoomId) {
|
||||||
|
@ -601,6 +664,15 @@ const onMessage = function(event) {
|
||||||
return;
|
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
|
// These APIs don't require userId
|
||||||
if (event.data.action === "join_rules_state") {
|
if (event.data.action === "join_rules_state") {
|
||||||
getJoinRules(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
|
@ -611,12 +683,6 @@ const onMessage = function(event) {
|
||||||
} else if (event.data.action === "get_membership_count") {
|
} else if (event.data.action === "get_membership_count") {
|
||||||
getMembershipCount(event, roomId);
|
getMembershipCount(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_widget") {
|
|
||||||
setWidget(event, roomId);
|
|
||||||
return;
|
|
||||||
} else if (event.data.action === "get_widgets") {
|
|
||||||
getWidgets(event, roomId);
|
|
||||||
return;
|
|
||||||
} else if (event.data.action === "get_room_enc_state") {
|
} else if (event.data.action === "get_room_enc_state") {
|
||||||
getRoomEncState(event, roomId);
|
getRoomEncState(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -21,6 +21,13 @@ const DEFAULTS = {
|
||||||
integrations_rest_url: "https://scalar.vector.im/api",
|
integrations_rest_url: "https://scalar.vector.im/api",
|
||||||
// Where to send bug reports. If not specified, bugs cannot be sent.
|
// Where to send bug reports. If not specified, bugs cannot be sent.
|
||||||
bug_report_endpoint_url: null,
|
bug_report_endpoint_url: null,
|
||||||
|
|
||||||
|
piwik: {
|
||||||
|
url: "https://piwik.riot.im/",
|
||||||
|
whitelistedHSUrls: ["https://matrix.org"],
|
||||||
|
whitelistedISUrls: ["https://vector.im", "https://matrix.org"],
|
||||||
|
siteId: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdkConfig {
|
class SdkConfig {
|
||||||
|
@ -45,3 +52,4 @@ class SdkConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SdkConfig;
|
module.exports = SdkConfig;
|
||||||
|
module.exports.DEFAULTS = DEFAULTS;
|
||||||
|
|
|
@ -96,6 +96,8 @@ const commands = {
|
||||||
colorScheme.primary_color = matches[1];
|
colorScheme.primary_color = matches[1];
|
||||||
if (matches[4]) {
|
if (matches[4]) {
|
||||||
colorScheme.secondary_color = matches[4];
|
colorScheme.secondary_color = matches[4];
|
||||||
|
} else {
|
||||||
|
colorScheme.secondary_color = colorScheme.primary_color;
|
||||||
}
|
}
|
||||||
return success(
|
return success(
|
||||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||||
|
@ -295,7 +297,7 @@ const commands = {
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const matches = args.match(/^(\S+?)( +(\d+))?$/);
|
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||||
let powerLevel = 50; // default power level for op
|
let powerLevel = 50; // default power level for op
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const userId = matches[1];
|
const userId = matches[1];
|
||||||
|
|
|
@ -52,8 +52,7 @@ function textForMemberEvent(ev) {
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent && prevContent.membership === 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
|
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
||||||
senderName,
|
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
|
|
|
@ -252,7 +252,6 @@ class Tinter {
|
||||||
|
|
||||||
|
|
||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
console.trace("setTheme " + theme);
|
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
|
|
||||||
// update keyRgb from the current theme CSS itself, if it defines it
|
// update keyRgb from the current theme CSS itself, if it defines it
|
||||||
|
@ -299,56 +298,66 @@ class Tinter {
|
||||||
|
|
||||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||||
const ss = document.styleSheets[i];
|
const ss = document.styleSheets[i];
|
||||||
if (!ss) continue; // well done safari >:(
|
try {
|
||||||
// Chromium apparently sometimes returns null here; unsure why.
|
if (!ss) continue; // well done safari >:(
|
||||||
// see $14534907369972FRXBx:matrix.org in HQ
|
// Chromium apparently sometimes returns null here; unsure why.
|
||||||
// ...ah, it's because there's a third party extension like
|
// see $14534907369972FRXBx:matrix.org in HQ
|
||||||
// privacybadger inserting its own stylesheet in there with a
|
// ...ah, it's because there's a third party extension like
|
||||||
// resource:// URI or something which results in a XSS error.
|
// privacybadger inserting its own stylesheet in there with a
|
||||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
// resource:// URI or something which results in a XSS error.
|
||||||
// ...except some browsers apparently return stylesheets without
|
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
||||||
// hrefs, which we have no choice but ignore right now
|
// ...except some browsers apparently return stylesheets without
|
||||||
|
// hrefs, which we have no choice but ignore right now
|
||||||
|
|
||||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
||||||
// here?
|
// here?
|
||||||
//
|
//
|
||||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
// Why do we need to limit it to vector's CSS file anyway - if there
|
||||||
// are other CSS files affecting the doc don't we want to apply the
|
// are other CSS files affecting the doc don't we want to apply the
|
||||||
// same transformations to them?
|
// same transformations to them?
|
||||||
//
|
//
|
||||||
// Iterating through the CSS looking for matches to hack on feels
|
// Iterating through the CSS looking for matches to hack on feels
|
||||||
// pretty horrible anyway. And what if the application skin doesn't use
|
// pretty horrible anyway. And what if the application skin doesn't use
|
||||||
// Vector Green as its primary color?
|
// Vector Green as its primary color?
|
||||||
// --richvdh
|
// --richvdh
|
||||||
|
|
||||||
// Yes, tinting assumes that you are using the Riot skin for now.
|
// Yes, tinting assumes that you are using the Riot skin for now.
|
||||||
// The right solution will be to move the CSS over to react-sdk.
|
// The right solution will be to move the CSS over to react-sdk.
|
||||||
// And yes, the default assets for the base skin might as well use
|
// And yes, the default assets for the base skin might as well use
|
||||||
// Vector Green as any other colour.
|
// Vector Green as any other colour.
|
||||||
// --matthew
|
// --matthew
|
||||||
|
|
||||||
if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
|
// stylesheets we don't have permission to access (eg. ones from extensions) have a null
|
||||||
if (ss.disabled) continue;
|
// href and will throw exceptions if we try to access their rules.
|
||||||
if (!ss.cssRules) continue;
|
if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
|
||||||
|
if (ss.disabled) continue;
|
||||||
|
if (!ss.cssRules) continue;
|
||||||
|
|
||||||
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
||||||
|
|
||||||
for (let j = 0; j < ss.cssRules.length; j++) {
|
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||||
const rule = ss.cssRules[j];
|
const rule = ss.cssRules[j];
|
||||||
if (!rule.style) continue;
|
if (!rule.style) continue;
|
||||||
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
||||||
for (let k = 0; k < this.cssAttrs.length; k++) {
|
for (let k = 0; k < this.cssAttrs.length; k++) {
|
||||||
const attr = this.cssAttrs[k];
|
const attr = this.cssAttrs[k];
|
||||||
for (let l = 0; l < this.keyRgb.length; l++) {
|
for (let l = 0; l < this.keyRgb.length; l++) {
|
||||||
if (rule.style[attr] === this.keyRgb[l]) {
|
if (rule.style[attr] === this.keyRgb[l]) {
|
||||||
this.cssFixups[this.theme].push({
|
this.cssFixups[this.theme].push({
|
||||||
style: rule.style,
|
style: rule.style,
|
||||||
attr: attr,
|
attr: attr,
|
||||||
index: l,
|
index: l,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Catch any random exceptions that happen here: all sorts of things can go
|
||||||
|
// wrong with this (nulls, SecurityErrors) and mostly it's for other
|
||||||
|
// stylesheets that we don't want to proces anyway. We should not propagate an
|
||||||
|
// exception out since this will cause the app to fail to start.
|
||||||
|
console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|
86
src/ToWidgetPostMessageApi.js
Normal file
86
src/ToWidgetPostMessageApi.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
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 Promise from "bluebird";
|
||||||
|
|
||||||
|
// const OUTBOUND_API_NAME = 'toWidget';
|
||||||
|
|
||||||
|
// Initiate requests using the "toWidget" postMessage API and handle responses
|
||||||
|
// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
|
||||||
|
// response field
|
||||||
|
export default class ToWidgetPostMessageApi {
|
||||||
|
constructor(timeoutMs) {
|
||||||
|
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
|
||||||
|
this._counter = 0;
|
||||||
|
this._requestMap = {
|
||||||
|
// $ID: {resolve, reject}
|
||||||
|
};
|
||||||
|
this.start = this.start.bind(this);
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
this.onPostMessage = this.onPostMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
window.addEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
window.removeEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPostMessage(ev) {
|
||||||
|
// THIS IS ALL UNSAFE EXECUTION.
|
||||||
|
// We do not verify who the sender of `ev` is!
|
||||||
|
const payload = ev.data;
|
||||||
|
// NOTE: Workaround for running in a mobile WebView where a
|
||||||
|
// postMessage immediately triggers this callback even though it is
|
||||||
|
// not the response.
|
||||||
|
if (payload.response === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const promise = this._requestMap[payload._id];
|
||||||
|
if (!promise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this._requestMap[payload._id];
|
||||||
|
promise.resolve(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate outbound requests (toWidget)
|
||||||
|
exec(action, targetWindow, targetOrigin) {
|
||||||
|
targetWindow = targetWindow || window.parent; // default to parent window
|
||||||
|
targetOrigin = targetOrigin || "*";
|
||||||
|
this._counter += 1;
|
||||||
|
action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._requestMap[action._id] = {resolve, reject};
|
||||||
|
targetWindow.postMessage(action, targetOrigin);
|
||||||
|
|
||||||
|
if (this._timeoutMs > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this._requestMap[action._id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
|
||||||
|
this._requestMap);
|
||||||
|
this._requestMap[action._id].reject(new Error("Timed out"));
|
||||||
|
delete this._requestMap[action._id];
|
||||||
|
}, this._timeoutMs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,8 @@ module.exports = {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType() == 'm.room.member') {
|
} else if (ev.getType() == 'm.room.member') {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (ev.getType() == 'm.room.third_party_invite') {
|
||||||
|
return false;
|
||||||
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDom = require('react-dom');
|
const ReactDom = require('react-dom');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
const Velocity = require('velocity-vector');
|
const Velocity = require('velocity-vector');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,16 +15,16 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// either a list of child nodes, or a single child.
|
// either a list of child nodes, or a single child.
|
||||||
children: React.PropTypes.any,
|
children: PropTypes.any,
|
||||||
|
|
||||||
// optional transition information for changing existing children
|
// optional transition information for changing existing children
|
||||||
transition: React.PropTypes.object,
|
transition: PropTypes.object,
|
||||||
|
|
||||||
// a list of state objects to apply to each child node in turn
|
// a list of state objects to apply to each child node in turn
|
||||||
startStyles: React.PropTypes.array,
|
startStyles: PropTypes.array,
|
||||||
|
|
||||||
// a list of transition options from the corresponding startStyle
|
// a list of transition options from the corresponding startStyle
|
||||||
enterTransitionOpts: React.PropTypes.array,
|
enterTransitionOpts: PropTypes.array,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
|
|
@ -15,312 +15,91 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Listens for incoming postMessage requests from embedded widgets. The following API is exposed:
|
* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
|
||||||
{
|
* spec. details / documentation.
|
||||||
api: "widget",
|
|
||||||
action: "content_loaded",
|
|
||||||
widgetId: $WIDGET_ID,
|
|
||||||
data: {}
|
|
||||||
// additional request fields
|
|
||||||
}
|
|
||||||
|
|
||||||
The complete request object is returned to the caller with an additional "response" key like so:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "content_loaded",
|
|
||||||
widgetId: $WIDGET_ID,
|
|
||||||
data: {},
|
|
||||||
// additional request fields
|
|
||||||
response: { ... }
|
|
||||||
}
|
|
||||||
|
|
||||||
The "api" field is required to use this API, and must be set to "widget" in all requests.
|
|
||||||
|
|
||||||
The "action" determines the format of the request and response. All actions can return an error response.
|
|
||||||
|
|
||||||
Additional data can be sent as additional, abritrary fields. However, typically the data object should be used.
|
|
||||||
|
|
||||||
A success response is an object with zero or more keys.
|
|
||||||
|
|
||||||
An error response is a "response" object which consists of a sole "error" key to indicate an error.
|
|
||||||
They look like:
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: "Unable to invite user into room.",
|
|
||||||
_error: <Original Error Object>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
The "message" key should be a human-friendly string.
|
|
||||||
|
|
||||||
ACTIONS
|
|
||||||
=======
|
|
||||||
** All actions must include an "api" field with valie "widget".**
|
|
||||||
All actions can return an error response instead of the response outlined below.
|
|
||||||
|
|
||||||
content_loaded
|
|
||||||
--------------
|
|
||||||
Indicates that widget contet has fully loaded
|
|
||||||
|
|
||||||
Request:
|
|
||||||
- widgetId is the unique ID of the widget instance in riot / matrix state.
|
|
||||||
- No additional fields.
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
success: true
|
|
||||||
}
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "content_loaded",
|
|
||||||
widgetId: $WIDGET_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
api_version
|
|
||||||
-----------
|
|
||||||
Get the current version of the widget postMessage API
|
|
||||||
|
|
||||||
Request:
|
|
||||||
- No additional fields.
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
api_version: "0.0.1"
|
|
||||||
}
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "api_version",
|
|
||||||
}
|
|
||||||
|
|
||||||
supported_api_versions
|
|
||||||
----------------------
|
|
||||||
Get versions of the widget postMessage API that are currently supported
|
|
||||||
|
|
||||||
Request:
|
|
||||||
- No additional fields.
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
api: "widget"
|
|
||||||
supported_versions: ["0.0.1"]
|
|
||||||
}
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "supported_api_versions",
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import URL from 'url';
|
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
||||||
|
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
if (!global.mxFromWidgetMessaging) {
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
||||||
'0.0.1',
|
global.mxFromWidgetMessaging.start();
|
||||||
];
|
|
||||||
|
|
||||||
import dis from './dispatcher';
|
|
||||||
|
|
||||||
if (!global.mxWidgetMessagingListenerCount) {
|
|
||||||
global.mxWidgetMessagingListenerCount = 0;
|
|
||||||
}
|
}
|
||||||
if (!global.mxWidgetMessagingMessageEndpoints) {
|
if (!global.mxToWidgetMessaging) {
|
||||||
global.mxWidgetMessagingMessageEndpoints = [];
|
global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
|
||||||
|
global.mxToWidgetMessaging.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OUTBOUND_API_NAME = 'toWidget';
|
||||||
|
|
||||||
/**
|
export default class WidgetMessaging {
|
||||||
* Register widget message event listeners
|
constructor(widgetId, widgetUrl, target) {
|
||||||
*/
|
|
||||||
function startListening() {
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
window.addEventListener("message", onMessage, false);
|
|
||||||
}
|
|
||||||
global.mxWidgetMessagingListenerCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register widget message event listeners
|
|
||||||
*/
|
|
||||||
function stopListening() {
|
|
||||||
global.mxWidgetMessagingListenerCount -= 1;
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
window.removeEventListener("message", onMessage);
|
|
||||||
}
|
|
||||||
if (global.mxWidgetMessagingListenerCount < 0) {
|
|
||||||
// Make an error so we get a stack trace
|
|
||||||
const e = new Error(
|
|
||||||
"WidgetMessaging: mismatched startListening / stopListening detected." +
|
|
||||||
" Negative count",
|
|
||||||
);
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a widget endpoint for trusted postMessage communication
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
*/
|
|
||||||
function addEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn("Invalid origin:", endpointUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
const endpoint = new WidgetMessageEndpoint(widgetId, origin);
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints) {
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
|
|
||||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
|
||||||
})) {
|
|
||||||
// Message endpoint already registered
|
|
||||||
console.warn("Endpoint already registered");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register a widget endpoint from trusted communication sources
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
* @return {boolean} True if endpoint was successfully removed
|
|
||||||
*/
|
|
||||||
function removeEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn("Invalid origin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
|
|
||||||
const length = global.mxWidgetMessagingMessageEndpoints.length;
|
|
||||||
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) {
|
|
||||||
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
|
||||||
});
|
|
||||||
return (length > global.mxWidgetMessagingMessageEndpoints.length);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle widget postMessage events
|
|
||||||
* @param {Event} event Event to handle
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
function onMessage(event) {
|
|
||||||
if (!event.origin) { // Handle chrome
|
|
||||||
event.origin = event.originalEvent.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event origin is empty string if undefined
|
|
||||||
if (
|
|
||||||
event.origin.length === 0 ||
|
|
||||||
!trustedEndpoint(event.origin) ||
|
|
||||||
event.data.api !== "widget" ||
|
|
||||||
!event.data.widgetId
|
|
||||||
) {
|
|
||||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = event.data.action;
|
|
||||||
const widgetId = event.data.widgetId;
|
|
||||||
if (action === 'content_loaded') {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'widget_content_loaded',
|
|
||||||
widgetId: widgetId,
|
|
||||||
});
|
|
||||||
sendResponse(event, {success: true});
|
|
||||||
} else if (action === 'supported_api_versions') {
|
|
||||||
sendResponse(event, {
|
|
||||||
api: "widget",
|
|
||||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
||||||
});
|
|
||||||
} else if (action === 'api_version') {
|
|
||||||
sendResponse(event, {
|
|
||||||
api: "widget",
|
|
||||||
version: WIDGET_API_VERSION,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn("Widget postMessage event unhandled");
|
|
||||||
sendError(event, {message: "The postMessage was unhandled"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if message origin is registered as trusted
|
|
||||||
* @param {string} origin PostMessage origin to check
|
|
||||||
* @return {boolean} True if trusted
|
|
||||||
*/
|
|
||||||
function trustedEndpoint(origin) {
|
|
||||||
if (!origin) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
|
|
||||||
return endpoint.endpointUrl === origin;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a postmessage response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {Object} res Response data
|
|
||||||
*/
|
|
||||||
function sendResponse(event, res) {
|
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
|
||||||
data.response = res;
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an error response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {string} msg Error message
|
|
||||||
* @param {Error} nestedError Nested error event (optional)
|
|
||||||
*/
|
|
||||||
function sendError(event, msg, nestedError) {
|
|
||||||
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
|
||||||
data.response = {
|
|
||||||
error: {
|
|
||||||
message: msg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (nestedError) {
|
|
||||||
data.response.error._error = nestedError;
|
|
||||||
}
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
|
||||||
*/
|
|
||||||
class WidgetMessageEndpoint {
|
|
||||||
/**
|
|
||||||
* Mapping of widget instance to URL for trusted postMessage communication.
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin.
|
|
||||||
*/
|
|
||||||
constructor(widgetId, endpointUrl) {
|
|
||||||
if (!widgetId) {
|
|
||||||
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
if (!endpointUrl) {
|
|
||||||
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
this.widgetId = widgetId;
|
this.widgetId = widgetId;
|
||||||
this.endpointUrl = endpointUrl;
|
this.widgetUrl = widgetUrl;
|
||||||
|
this.target = target;
|
||||||
|
this.fromWidget = global.mxFromWidgetMessaging;
|
||||||
|
this.toWidget = global.mxToWidgetMessaging;
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
messageToWidget(action) {
|
||||||
|
return this.toWidget.exec(action, this.target).then((data) => {
|
||||||
|
// Check for errors and reject if found
|
||||||
|
if (data.response === undefined) { // null is valid
|
||||||
|
throw new Error("Missing 'response' field");
|
||||||
|
}
|
||||||
|
if (data.response && data.response.error) {
|
||||||
|
const err = data.response.error;
|
||||||
|
const msg = String(err.message ? err.message : "An error was returned");
|
||||||
|
if (err._error) {
|
||||||
|
console.error(err._error);
|
||||||
|
}
|
||||||
|
// Potential XSS attack if 'msg' is not appropriately sanitized,
|
||||||
|
// as it is untrusted input by our parent window (which we assume is Riot).
|
||||||
|
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
// Return the response field for the request
|
||||||
|
return data.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a screenshot from a widget
|
||||||
|
* @return {Promise} To be resolved with screenshot data when it has been generated
|
||||||
|
*/
|
||||||
|
getScreenshot() {
|
||||||
|
console.warn('Requesting screenshot for', this.widgetId);
|
||||||
|
return this.messageToWidget({
|
||||||
|
api: OUTBOUND_API_NAME,
|
||||||
|
action: "screenshot",
|
||||||
|
})
|
||||||
|
.catch((error) => new Error("Failed to get screenshot: " + error.message))
|
||||||
|
.then((response) => response.screenshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request capabilities required by the widget
|
||||||
|
* @return {Promise} To be resolved with an array of requested widget capabilities
|
||||||
|
*/
|
||||||
|
getCapabilities() {
|
||||||
|
console.warn('Requesting capabilities for', this.widgetId);
|
||||||
|
return this.messageToWidget({
|
||||||
|
api: OUTBOUND_API_NAME,
|
||||||
|
action: "capabilities",
|
||||||
|
}).then((response) => {
|
||||||
|
console.warn('Got capabilities for', this.widgetId, response.capabilities);
|
||||||
|
return response.capabilities;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
startListening: startListening,
|
|
||||||
stopListening: stopListening,
|
|
||||||
addEndpoint: addEndpoint,
|
|
||||||
removeEndpoint: removeEndpoint,
|
|
||||||
};
|
|
||||||
|
|
37
src/WidgetMessagingEndpoint.js
Normal file
37
src/WidgetMessagingEndpoint.js
Normal 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
||||||
|
*/
|
||||||
|
export default class WidgetMessageEndpoint {
|
||||||
|
/**
|
||||||
|
* Mapping of widget instance to URL for trusted postMessage communication.
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin.
|
||||||
|
*/
|
||||||
|
constructor(widgetId, endpointUrl) {
|
||||||
|
if (!widgetId) {
|
||||||
|
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
||||||
|
}
|
||||||
|
if (!endpointUrl) {
|
||||||
|
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
||||||
|
}
|
||||||
|
this.widgetId = widgetId;
|
||||||
|
this.endpointUrl = endpointUrl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
export default class WidgetUtils {
|
export default class WidgetUtils {
|
||||||
|
|
||||||
/* Returns true if user is able to send state events to modify widgets in this room
|
/* 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
|
* @param roomId -- The ID of the room to check
|
||||||
* @return Boolean -- true if the user can modify widgets in this room
|
* @return Boolean -- true if the user can modify widgets in this room
|
||||||
* @throws Error -- specifies the error reason
|
* @throws Error -- specifies the error reason
|
||||||
|
|
|
@ -62,6 +62,127 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef RoomAction
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action 'MatrixActions.Room'.
|
||||||
|
* @property {Room} room the Room that was stored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MatrixActions.Room action that represents a MatrixClient `Room`
|
||||||
|
* matrix event, emitted when a Room is stored in the client.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client.
|
||||||
|
* @param {Room} room the Room that was stored.
|
||||||
|
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||||
|
*/
|
||||||
|
function createRoomAction(matrixClient, room) {
|
||||||
|
return { action: 'MatrixActions.Room', room };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef RoomTagsAction
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action 'MatrixActions.Room.tags'.
|
||||||
|
* @property {Room} room the Room whose tags changed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MatrixActions.Room.tags action that represents a MatrixClient
|
||||||
|
* `Room.tags` matrix event, emitted when the m.tag room account data
|
||||||
|
* event is updated.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client.
|
||||||
|
* @param {MatrixEvent} roomTagsEvent the m.tag event.
|
||||||
|
* @param {Room} room the Room whose tags were changed.
|
||||||
|
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
||||||
|
*/
|
||||||
|
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||||
|
return { action: 'MatrixActions.Room.tags', room };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef RoomTimelineAction
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action 'MatrixActions.Room.timeline'.
|
||||||
|
* @property {boolean} isLiveEvent whether the event was attached to a
|
||||||
|
* live timeline.
|
||||||
|
* @property {boolean} isLiveUnfilteredRoomTimelineEvent whether the
|
||||||
|
* event was attached to a timeline in the set of unfiltered timelines.
|
||||||
|
* @property {Room} room the Room whose tags changed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MatrixActions.Room.timeline action that represents a
|
||||||
|
* MatrixClient `Room.timeline` matrix event, emitted when an event
|
||||||
|
* is added to or removed from a timeline of a room.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client.
|
||||||
|
* @param {MatrixEvent} timelineEvent the event that was added/removed.
|
||||||
|
* @param {Room} room the Room that was stored.
|
||||||
|
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||||
|
* to the start (and not the end) of the timeline.
|
||||||
|
* @param {boolean} removed whether the event was removed from the
|
||||||
|
* timeline.
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {boolean} data.liveEvent whether the event is a live event,
|
||||||
|
* belonging to a live timeline.
|
||||||
|
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||||
|
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
||||||
|
*/
|
||||||
|
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
|
||||||
|
return {
|
||||||
|
action: 'MatrixActions.Room.timeline',
|
||||||
|
event: timelineEvent,
|
||||||
|
isLiveEvent: data.liveEvent,
|
||||||
|
isLiveUnfilteredRoomTimelineEvent:
|
||||||
|
room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef RoomMembershipAction
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action 'MatrixActions.RoomMember.membership'.
|
||||||
|
* @property {RoomMember} member the member whose membership was updated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MatrixActions.RoomMember.membership action that represents
|
||||||
|
* a MatrixClient `RoomMember.membership` matrix event, emitted when a
|
||||||
|
* member's membership is updated.
|
||||||
|
*
|
||||||
|
* @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`.
|
||||||
|
*/
|
||||||
|
function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) {
|
||||||
|
return { action: 'MatrixActions.RoomMember.membership', member };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef EventDecryptedAction
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action 'MatrixActions.Event.decrypted'.
|
||||||
|
* @property {MatrixEvent} event the matrix event that was decrypted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MatrixActions.Event.decrypted action that represents
|
||||||
|
* a MatrixClient `Event.decrypted` matrix event, emitted when a
|
||||||
|
* matrix event is decrypted.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client.
|
||||||
|
* @param {MatrixEvent} event the matrix event that was decrypted.
|
||||||
|
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
||||||
|
*/
|
||||||
|
function createEventDecryptedAction(matrixClient, event) {
|
||||||
|
return { action: 'MatrixActions.Event.decrypted', event };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This object is responsible for dispatching actions when certain events are emitted by
|
* This object is responsible for dispatching actions when certain events are emitted by
|
||||||
* the given MatrixClient.
|
* the given MatrixClient.
|
||||||
|
@ -78,6 +199,11 @@ export default {
|
||||||
start(matrixClient) {
|
start(matrixClient) {
|
||||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||||
|
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, 'Event.decrypted', createEventDecryptedAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +217,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
||||||
const listener = (...args) => {
|
const listener = (...args) => {
|
||||||
dis.dispatch(actionCreator(matrixClient, ...args));
|
dis.dispatch(actionCreator(matrixClient, ...args), true);
|
||||||
};
|
};
|
||||||
matrixClient.on(eventName, listener);
|
matrixClient.on(eventName, listener);
|
||||||
this._matrixClientListenersStop.push(() => {
|
this._matrixClientListenersStop.push(() => {
|
||||||
|
|
146
src/actions/RoomListActions.js
Normal file
146
src/actions/RoomListActions.js
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
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 { asyncAction } from './actionCreators';
|
||||||
|
import RoomListStore from '../stores/RoomListStore';
|
||||||
|
|
||||||
|
import Modal from '../Modal';
|
||||||
|
import * as Rooms from '../Rooms';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
|
const RoomListActions = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an action thunk that will do an asynchronous request to
|
||||||
|
* tag room.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||||
|
* account data on.
|
||||||
|
* @param {Room} room the room to tag.
|
||||||
|
* @param {string} oldTag the tag to remove (unless oldTag ==== newTag)
|
||||||
|
* @param {string} newTag the tag with which to tag the room.
|
||||||
|
* @param {?number} oldIndex the previous position of the room in the
|
||||||
|
* list of rooms.
|
||||||
|
* @param {?number} newIndex the new position of the room in the list
|
||||||
|
* of rooms.
|
||||||
|
* @returns {function} an action thunk.
|
||||||
|
* @see asyncAction
|
||||||
|
*/
|
||||||
|
RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) {
|
||||||
|
let metaData = null;
|
||||||
|
|
||||||
|
// Is the tag ordered manually?
|
||||||
|
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||||
|
const lists = RoomListStore.getRoomLists();
|
||||||
|
const newList = [...lists[newTag]];
|
||||||
|
|
||||||
|
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||||
|
|
||||||
|
// If the room was moved "down" (increasing index) in the same list we
|
||||||
|
// need to use the orders of the tiles with indices shifted by +1
|
||||||
|
const offset = (
|
||||||
|
newTag === oldTag && oldIndex < newIndex
|
||||||
|
) ? 1 : 0;
|
||||||
|
|
||||||
|
const indexBefore = offset + newIndex - 1;
|
||||||
|
const indexAfter = offset + newIndex;
|
||||||
|
|
||||||
|
const prevOrder = indexBefore <= 0 ?
|
||||||
|
0 : newList[indexBefore].tags[newTag].order;
|
||||||
|
const nextOrder = indexAfter >= newList.length ?
|
||||||
|
1 : newList[indexAfter].tags[newTag].order;
|
||||||
|
|
||||||
|
metaData = {
|
||||||
|
order: (prevOrder + nextOrder) / 2.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncAction('RoomListActions.tagRoom', () => {
|
||||||
|
const promises = [];
|
||||||
|
const roomId = room.roomId;
|
||||||
|
|
||||||
|
// Evil hack to get DMs behaving
|
||||||
|
if ((oldTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||||
|
(oldTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||||
|
) {
|
||||||
|
return Rooms.guessAndSetDMRoom(
|
||||||
|
room, newTag === 'im.vector.fake.direct',
|
||||||
|
).catch((err) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to set direct chat tag " + err);
|
||||||
|
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||||
|
title: _t('Failed to set direct chat tag'),
|
||||||
|
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChangedSubLists = oldTag !== newTag;
|
||||||
|
|
||||||
|
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||||
|
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||||
|
//
|
||||||
|
// if we moved lists, remove the old tag
|
||||||
|
if (oldTag && oldTag !== 'im.vector.fake.direct' &&
|
||||||
|
hasChangedSubLists
|
||||||
|
) {
|
||||||
|
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||||
|
roomId, oldTag,
|
||||||
|
).catch(function(err) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to remove tag " + oldTag + " from room: " + err);
|
||||||
|
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||||
|
title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}),
|
||||||
|
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(promiseToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we moved lists or the ordering changed, add the new tag
|
||||||
|
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||||
|
(hasChangedSubLists || metaData)
|
||||||
|
) {
|
||||||
|
// metaData is the body of the PUT to set the tag, so it must
|
||||||
|
// at least be an empty object.
|
||||||
|
metaData = metaData || {};
|
||||||
|
|
||||||
|
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||||
|
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||||
|
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||||
|
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||||
|
});
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(promiseToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, () => {
|
||||||
|
// For an optimistic update
|
||||||
|
return {
|
||||||
|
room, oldTag, newTag, metaData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomListActions;
|
|
@ -22,25 +22,87 @@ const TagOrderActions = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an action thunk that will do an asynchronous request to
|
* Creates an action thunk that will do an asynchronous request to
|
||||||
* commit TagOrderStore.getOrderedTags() to account data and dispatch
|
* move a tag in TagOrderStore to destinationIx.
|
||||||
* actions to indicate the status of the request.
|
|
||||||
*
|
*
|
||||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||||
* account data on.
|
* account data on.
|
||||||
|
* @param {string} tag the tag to move.
|
||||||
|
* @param {number} destinationIx the new position of the tag.
|
||||||
* @returns {function} an action thunk that will dispatch actions
|
* @returns {function} an action thunk that will dispatch actions
|
||||||
* indicating the status of the request.
|
* indicating the status of the request.
|
||||||
* @see asyncAction
|
* @see asyncAction
|
||||||
*/
|
*/
|
||||||
TagOrderActions.commitTagOrdering = function(matrixClient) {
|
TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
||||||
return asyncAction('TagOrderActions.commitTagOrdering', () => {
|
// Only commit tags if the state is ready, i.e. not null
|
||||||
// Only commit tags if the state is ready, i.e. not null
|
let tags = TagOrderStore.getOrderedTags();
|
||||||
const tags = TagOrderStore.getOrderedTags();
|
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags = tags.filter((t) => t !== tag);
|
||||||
|
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
||||||
|
|
||||||
|
removedTags = removedTags.filter((t) => t !== tag);
|
||||||
|
|
||||||
|
const storeId = TagOrderStore.getStoreId();
|
||||||
|
|
||||||
|
return asyncAction('TagOrderActions.moveTag', () => {
|
||||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||||
return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags});
|
return matrixClient.setAccountData(
|
||||||
|
'im.vector.web.tag_ordering',
|
||||||
|
{tags, removedTags, _storeId: storeId},
|
||||||
|
);
|
||||||
|
}, () => {
|
||||||
|
// For an optimistic update
|
||||||
|
return {tags, removedTags};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an action thunk that will do an asynchronous request to
|
||||||
|
* label a tag as removed in im.vector.web.tag_ordering account data.
|
||||||
|
*
|
||||||
|
* The reason this is implemented with new state `removedTags` is that
|
||||||
|
* we incrementally and initially populate `tags` with groups that
|
||||||
|
* have been joined. If we remove a group from `tags`, it will just
|
||||||
|
* get added (as it looks like a group we've recently joined).
|
||||||
|
*
|
||||||
|
* NB: If we ever support adding of tags (which is planned), we should
|
||||||
|
* take special care to remove the tag from `removedTags` when we add
|
||||||
|
* it.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||||
|
* account data on.
|
||||||
|
* @param {string} tag the tag to remove.
|
||||||
|
* @returns {function} an action thunk that will dispatch actions
|
||||||
|
* indicating the status of the request.
|
||||||
|
* @see asyncAction
|
||||||
|
*/
|
||||||
|
TagOrderActions.removeTag = function(matrixClient, tag) {
|
||||||
|
// Don't change tags, just removedTags
|
||||||
|
const tags = TagOrderStore.getOrderedTags();
|
||||||
|
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||||
|
|
||||||
|
if (removedTags.includes(tag)) {
|
||||||
|
// Return a thunk that doesn't do anything, we don't even need
|
||||||
|
// an asynchronous action here, the tag is already removed.
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
removedTags.push(tag);
|
||||||
|
|
||||||
|
const storeId = TagOrderStore.getStoreId();
|
||||||
|
|
||||||
|
return asyncAction('TagOrderActions.removeTag', () => {
|
||||||
|
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||||
|
return matrixClient.setAccountData(
|
||||||
|
'im.vector.web.tag_ordering',
|
||||||
|
{tags, removedTags, _storeId: storeId},
|
||||||
|
);
|
||||||
|
}, () => {
|
||||||
|
// For an optimistic update
|
||||||
|
return {removedTags};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,32 @@ limitations under the License.
|
||||||
* suffix determining whether it is pending, successful or
|
* suffix determining whether it is pending, successful or
|
||||||
* a failure.
|
* a failure.
|
||||||
* @param {function} fn a function that returns a Promise.
|
* @param {function} fn a function that returns a Promise.
|
||||||
|
* @param {function?} pendingFn a function that returns an object to assign
|
||||||
|
* to the `request` key of the ${id}.pending
|
||||||
|
* payload.
|
||||||
* @returns {function} an action thunk - a function that uses its single
|
* @returns {function} an action thunk - a function that uses its single
|
||||||
* argument as a dispatch function to dispatch the
|
* argument as a dispatch function to dispatch the
|
||||||
* following actions:
|
* following actions:
|
||||||
* `${id}.pending` and either
|
* `${id}.pending` and either
|
||||||
* `${id}.success` or
|
* `${id}.success` or
|
||||||
* `${id}.failure`.
|
* `${id}.failure`.
|
||||||
|
*
|
||||||
|
* The shape of each are:
|
||||||
|
* { action: '${id}.pending', request }
|
||||||
|
* { action: '${id}.success', result }
|
||||||
|
* { action: '${id}.failure', err }
|
||||||
|
*
|
||||||
|
* where `request` is returned by `pendingFn` and
|
||||||
|
* result is the result of the promise returned by
|
||||||
|
* `fn`.
|
||||||
*/
|
*/
|
||||||
export function asyncAction(id, fn) {
|
export function asyncAction(id, fn, pendingFn) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch({action: id + '.pending'});
|
dispatch({
|
||||||
|
action: id + '.pending',
|
||||||
|
request:
|
||||||
|
typeof pendingFn === 'function' ? pendingFn() : undefined,
|
||||||
|
});
|
||||||
fn().then((result) => {
|
fn().then((result) => {
|
||||||
dispatch({action: id + '.success', result});
|
dispatch({action: id + '.success', result});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
@ -23,8 +24,8 @@ module.exports = React.createClass({
|
||||||
displayName: 'EncryptedEventDialog',
|
displayName: 'EncryptedEventDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
event: React.PropTypes.object.isRequired,
|
event: PropTypes.object.isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
|
@ -29,8 +30,8 @@ export default React.createClass({
|
||||||
displayName: 'ExportE2eKeysDialog',
|
displayName: 'ExportE2eKeysDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
|
@ -40,8 +41,8 @@ export default React.createClass({
|
||||||
displayName: 'ImportE2eKeysDialog',
|
displayName: 'ImportE2eKeysDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -105,6 +105,11 @@ const COMMANDS = [
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: _td('Stops ignoring a user, showing their messages going forward'),
|
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
|
// Omitting `/markdown` as it only seems to apply to OldComposer
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
/* These were earlier stateless functional components but had to be converted
|
/* These were earlier stateless functional components but had to be converted
|
||||||
|
@ -42,10 +43,10 @@ export class TextualCompletion extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextualCompletion.propTypes = {
|
TextualCompletion.propTypes = {
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
subtitle: React.PropTypes.string,
|
subtitle: PropTypes.string,
|
||||||
description: React.PropTypes.string,
|
description: PropTypes.string,
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PillCompletion extends React.Component {
|
export class PillCompletion extends React.Component {
|
||||||
|
@ -69,9 +70,9 @@ export class PillCompletion extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PillCompletion.propTypes = {
|
PillCompletion.propTypes = {
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
subtitle: React.PropTypes.string,
|
subtitle: PropTypes.string,
|
||||||
description: React.PropTypes.string,
|
description: PropTypes.string,
|
||||||
initialComponent: React.PropTypes.element,
|
initialComponent: PropTypes.element,
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
import {makeRoomPermalink} from "../matrix-to";
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
suffix: ' ',
|
suffix: ' ',
|
||||||
href: 'https://matrix.to/#/' + displayAlias,
|
href: makeRoomPermalink(displayAlias),
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||||
),
|
),
|
||||||
|
|
|
@ -28,6 +28,7 @@ import _sortBy from 'lodash/sortBy';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
import type {Room, RoomMember} from 'matrix-js-sdk';
|
import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||||
|
import {makeUserPermalink} from "../matrix-to";
|
||||||
|
|
||||||
const USER_REGEX = /@\S*/g;
|
const USER_REGEX = /@\S*/g;
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.matcher = new FuzzyMatcher([], {
|
this.matcher = new FuzzyMatcher([], {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name', 'userId'],
|
||||||
shouldMatchPrefix: true,
|
shouldMatchPrefix: true,
|
||||||
|
shouldMatchWordsOnly: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
||||||
|
@ -71,6 +73,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
// updates from pagination will happen when the paginate completes.
|
// updates from pagination will happen when the paginate completes.
|
||||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||||
|
|
||||||
|
// TODO: lazyload if we have no ev.sender room member?
|
||||||
this.onUserSpoke(ev.sender);
|
this.onUserSpoke(ev.sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
// relies on the length of the entity === length of the text in the decoration.
|
// relies on the length of the entity === length of the text in the decoration.
|
||||||
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
||||||
suffix: range.start === 0 ? ': ' : ' ',
|
suffix: range.start === 0 ? ': ' : ' ',
|
||||||
href: 'https://matrix.to/#/' + user.userId,
|
href: makeUserPermalink(user.userId),
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion
|
<PillCompletion
|
||||||
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
|
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
|
||||||
|
@ -146,6 +149,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
onUserSpoke(user: RoomMember) {
|
onUserSpoke(user: RoomMember) {
|
||||||
if (this.users === null) return;
|
if (this.users === null) return;
|
||||||
|
if (!user) return;
|
||||||
if (user.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
// Move the user that spoke to the front of the array
|
// Move the user that spoke to the front of the array
|
||||||
|
@ -157,7 +161,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
{ completions }
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||||
const classNames = require('classnames');
|
const classNames = require('classnames');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -29,11 +30,21 @@ module.exports = {
|
||||||
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
menuWidth: React.PropTypes.number,
|
top: PropTypes.number,
|
||||||
menuHeight: React.PropTypes.number,
|
bottom: PropTypes.number,
|
||||||
chevronOffset: React.PropTypes.number,
|
left: PropTypes.number,
|
||||||
menuColour: React.PropTypes.string,
|
right: PropTypes.number,
|
||||||
chevronFace: React.PropTypes.string, // top, bottom, left, right
|
menuWidth: PropTypes.number,
|
||||||
|
menuHeight: PropTypes.number,
|
||||||
|
chevronOffset: PropTypes.number,
|
||||||
|
menuColour: PropTypes.string,
|
||||||
|
chevronFace: PropTypes.string, // top, bottom, left, right
|
||||||
|
// Function to be called on menu close
|
||||||
|
onFinished: PropTypes.func,
|
||||||
|
menuPaddingTop: PropTypes.number,
|
||||||
|
menuPaddingRight: PropTypes.number,
|
||||||
|
menuPaddingBottom: PropTypes.number,
|
||||||
|
menuPaddingLeft: PropTypes.number,
|
||||||
},
|
},
|
||||||
|
|
||||||
getOrCreateContainer: function() {
|
getOrCreateContainer: function() {
|
||||||
|
@ -51,14 +62,19 @@ module.exports = {
|
||||||
createMenu: function(Element, props) {
|
createMenu: function(Element, props) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const closeMenu = function() {
|
const closeMenu = function(...args) {
|
||||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||||
|
|
||||||
if (props && props.onFinished) {
|
if (props && props.onFinished) {
|
||||||
props.onFinished.apply(null, arguments);
|
props.onFinished.apply(null, args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close the menu on window resize
|
||||||
|
const windowResize = function() {
|
||||||
|
closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
const position = {};
|
const position = {};
|
||||||
let chevronFace = null;
|
let chevronFace = null;
|
||||||
|
|
||||||
|
@ -129,13 +145,26 @@ module.exports = {
|
||||||
menuStyle["backgroundColor"] = props.menuColour;
|
menuStyle["backgroundColor"] = props.menuColour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||||
|
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||||
|
}
|
||||||
|
if (!isNaN(Number(props.menuPaddingLeft))) {
|
||||||
|
menuStyle["paddingLeft"] = props.menuPaddingLeft;
|
||||||
|
}
|
||||||
|
if (!isNaN(Number(props.menuPaddingBottom))) {
|
||||||
|
menuStyle["paddingBottom"] = props.menuPaddingBottom;
|
||||||
|
}
|
||||||
|
if (!isNaN(Number(props.menuPaddingRight))) {
|
||||||
|
menuStyle["paddingRight"] = props.menuPaddingRight;
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||||
// property set here so you can't close the menu from a button click!
|
// property set here so you can't close the menu from a button click!
|
||||||
const menu = (
|
const menu = (
|
||||||
<div className={className} style={position}>
|
<div className={className} style={position}>
|
||||||
<div className={menuClasses} style={menuStyle}>
|
<div className={menuClasses} style={menuStyle}>
|
||||||
{ chevron }
|
{ chevron }
|
||||||
<Element {...props} onFinished={closeMenu} />
|
<Element {...props} onFinished={closeMenu} onResize={windowResize} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||||
<style>{ chevronCSS }</style>
|
<style>{ chevronCSS }</style>
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
@ -30,8 +31,8 @@ module.exports = React.createClass({
|
||||||
displayName: 'CreateRoom',
|
displayName: 'CreateRoom',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onRoomCreated: React.PropTypes.func,
|
onRoomCreated: PropTypes.func,
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
phases: {
|
phases: {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
@ -28,7 +29,7 @@ const FilePanel = React.createClass({
|
||||||
displayName: 'FilePanel',
|
displayName: 'FilePanel',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
roomId: React.PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -67,6 +68,9 @@ const FilePanel = React.createClass({
|
||||||
"room": {
|
"room": {
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"contains_url": true,
|
"contains_url": true,
|
||||||
|
"not_types": [
|
||||||
|
"m.sticker",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd.
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -29,8 +29,9 @@ import classnames from 'classnames';
|
||||||
|
|
||||||
import GroupStoreCache from '../../stores/GroupStoreCache';
|
import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
|
import FlairStore from '../../stores/FlairStore';
|
||||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
|
||||||
|
|
||||||
const LONG_DESC_PLACEHOLDER = _td(
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
`<h1>HTML for your community's page</h1>
|
`<h1>HTML for your community's page</h1>
|
||||||
|
@ -209,7 +210,7 @@ const FeaturedRoom = React.createClass({
|
||||||
|
|
||||||
let permalink = null;
|
let permalink = null;
|
||||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
permalink = makeGroupPermalink(this.props.summaryInfo.profile.canonical_alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomNameNode = null;
|
let roomNameNode = null;
|
||||||
|
@ -366,7 +367,7 @@ const FeaturedUser = React.createClass({
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||||
|
|
||||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
|
||||||
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||||
const httpUrl = MatrixClientPeg.get()
|
const httpUrl = MatrixClientPeg.get()
|
||||||
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||||
|
@ -390,7 +391,7 @@ const FeaturedUser = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
const GroupContext = {
|
const GroupContext = {
|
||||||
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CategoryRoomList.contextTypes = GroupContext;
|
CategoryRoomList.contextTypes = GroupContext;
|
||||||
|
@ -398,6 +399,9 @@ FeaturedRoom.contextTypes = GroupContext;
|
||||||
RoleUserList.contextTypes = GroupContext;
|
RoleUserList.contextTypes = GroupContext;
|
||||||
FeaturedUser.contextTypes = GroupContext;
|
FeaturedUser.contextTypes = GroupContext;
|
||||||
|
|
||||||
|
const GROUP_JOINPOLICY_OPEN = "open";
|
||||||
|
const GROUP_JOINPOLICY_INVITE = "invite";
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'GroupView',
|
displayName: 'GroupView',
|
||||||
|
|
||||||
|
@ -408,7 +412,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
groupStore: React.PropTypes.instanceOf(GroupStore),
|
groupStore: PropTypes.instanceOf(GroupStore),
|
||||||
},
|
},
|
||||||
|
|
||||||
getChildContext: function() {
|
getChildContext: function() {
|
||||||
|
@ -428,6 +432,7 @@ export default React.createClass({
|
||||||
editing: false,
|
editing: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
uploadingAvatar: false,
|
uploadingAvatar: false,
|
||||||
|
avatarChanged: false,
|
||||||
membershipBusy: false,
|
membershipBusy: false,
|
||||||
publicityBusy: false,
|
publicityBusy: false,
|
||||||
inviterProfile: null,
|
inviterProfile: null,
|
||||||
|
@ -461,6 +466,10 @@ export default React.createClass({
|
||||||
_onGroupMyMembership: function(group) {
|
_onGroupMyMembership: function(group) {
|
||||||
if (group.groupId !== this.props.groupId) return;
|
if (group.groupId !== this.props.groupId) return;
|
||||||
|
|
||||||
|
if (group.myMembership === 'leave') {
|
||||||
|
// Leave settings - the user might have clicked the "Leave" button
|
||||||
|
this._closeSettings();
|
||||||
|
}
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -543,6 +552,12 @@ export default React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: true,
|
editing: true,
|
||||||
profileForm: Object.assign({}, this.state.summary.profile),
|
profileForm: Object.assign({}, this.state.summary.profile),
|
||||||
|
joinableForm: {
|
||||||
|
policyType:
|
||||||
|
this.state.summary.profile.is_openly_joinable ?
|
||||||
|
GROUP_JOINPOLICY_OPEN :
|
||||||
|
GROUP_JOINPOLICY_INVITE,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'panel_disable',
|
action: 'panel_disable',
|
||||||
|
@ -551,6 +566,10 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCancelClick: function() {
|
_onCancelClick: function() {
|
||||||
|
this._closeSettings();
|
||||||
|
},
|
||||||
|
|
||||||
|
_closeSettings() {
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: false,
|
editing: false,
|
||||||
profileForm: null,
|
profileForm: null,
|
||||||
|
@ -589,6 +608,10 @@ export default React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadingAvatar: false,
|
uploadingAvatar: false,
|
||||||
profileForm: newProfileForm,
|
profileForm: newProfileForm,
|
||||||
|
|
||||||
|
// Indicate that FlairStore needs to be poked to show this change
|
||||||
|
// in TagTile (TagPanel), Flair and GroupTile (MyGroups).
|
||||||
|
avatarChanged: true,
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({uploadingAvatar: false});
|
this.setState({uploadingAvatar: false});
|
||||||
|
@ -601,11 +624,15 @@ export default React.createClass({
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onJoinableChange: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
joinableForm: { policyType: ev.target.value },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_onSaveClick: function() {
|
_onSaveClick: function() {
|
||||||
this.setState({saving: true});
|
this.setState({saving: true});
|
||||||
const savePromise = this.state.isUserPrivileged ?
|
const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
|
||||||
this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm) :
|
|
||||||
Promise.resolve();
|
|
||||||
savePromise.then((result) => {
|
savePromise.then((result) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
saving: false,
|
saving: false,
|
||||||
|
@ -614,6 +641,11 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
dis.dispatch({action: 'panel_disable'});
|
dis.dispatch({action: 'panel_disable'});
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
|
|
||||||
|
if (this.state.avatarChanged) {
|
||||||
|
// XXX: Evil - poking a store should be done from an async action
|
||||||
|
FlairStore.refreshGroupProfile(this._matrixClient, this.props.groupId);
|
||||||
|
}
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
saving: false,
|
saving: false,
|
||||||
|
@ -624,11 +656,27 @@ export default React.createClass({
|
||||||
title: _t('Error'),
|
title: _t('Error'),
|
||||||
description: _t('Failed to update community'),
|
description: _t('Failed to update community'),
|
||||||
});
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({
|
||||||
|
avatarChanged: false,
|
||||||
|
});
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
_onAcceptInviteClick: function() {
|
_saveGroup: async function() {
|
||||||
|
await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm);
|
||||||
|
await this._matrixClient.setGroupJoinPolicy(this.props.groupId, {
|
||||||
|
type: this.state.joinableForm.policyType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAcceptInviteClick: async function() {
|
||||||
this.setState({membershipBusy: true});
|
this.setState({membershipBusy: true});
|
||||||
|
|
||||||
|
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||||
|
// spinner disappearing after we have fetched new group data.
|
||||||
|
await Promise.delay(500);
|
||||||
|
|
||||||
this._groupStore.acceptGroupInvite().then(() => {
|
this._groupStore.acceptGroupInvite().then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
@ -641,9 +689,14 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onRejectInviteClick: function() {
|
_onRejectInviteClick: async function() {
|
||||||
this.setState({membershipBusy: true});
|
this.setState({membershipBusy: true});
|
||||||
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
|
|
||||||
|
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||||
|
// spinner disappearing after we have fetched new group data.
|
||||||
|
await Promise.delay(500);
|
||||||
|
|
||||||
|
this._groupStore.leaveGroup().then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
|
@ -655,6 +708,25 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onJoinClick: async function() {
|
||||||
|
this.setState({membershipBusy: true});
|
||||||
|
|
||||||
|
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||||
|
// spinner disappearing after we have fetched new group data.
|
||||||
|
await Promise.delay(500);
|
||||||
|
|
||||||
|
this._groupStore.joinGroup().then(() => {
|
||||||
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({membershipBusy: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Error joining room', '', ErrorDialog, {
|
||||||
|
title: _t("Error"),
|
||||||
|
description: _t("Unable to join community"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_onLeaveClick: function() {
|
_onLeaveClick: function() {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
||||||
|
@ -662,18 +734,23 @@ export default React.createClass({
|
||||||
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
||||||
button: _t("Leave"),
|
button: _t("Leave"),
|
||||||
danger: true,
|
danger: true,
|
||||||
onFinished: (confirmed) => {
|
onFinished: async (confirmed) => {
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
this.setState({membershipBusy: true});
|
this.setState({membershipBusy: true});
|
||||||
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
|
|
||||||
|
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||||
|
// spinner disappearing after we have fetched new group data.
|
||||||
|
await Promise.delay(500);
|
||||||
|
|
||||||
|
this._groupStore.leaveGroup().then(() => {
|
||||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, {
|
||||||
title: _t("Error"),
|
title: _t("Error"),
|
||||||
description: _t("Unable to leave room"),
|
description: _t("Unable to leave community"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -691,8 +768,22 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
|
const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
|
||||||
|
const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ?
|
||||||
|
<div className="mx_GroupView_changeDelayWarning">
|
||||||
|
{ _t(
|
||||||
|
'Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> ' +
|
||||||
|
'might not be seen by other users for up to 30 minutes.',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'bold1': (sub) => <b> { sub } </b>,
|
||||||
|
'bold2': (sub) => <b> { sub } </b>,
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
</div> : <div />;
|
||||||
return <div className={groupSettingsSectionClasses}>
|
return <div className={groupSettingsSectionClasses}>
|
||||||
{ header }
|
{ header }
|
||||||
|
{ changeDelayWarning }
|
||||||
|
{ this._getJoinableNode() }
|
||||||
{ this._getLongDescriptionNode() }
|
{ this._getLongDescriptionNode() }
|
||||||
{ this._getRoomsNode() }
|
{ this._getRoomsNode() }
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -831,9 +922,8 @@ export default React.createClass({
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
|
||||||
const group = this._matrixClient.getGroup(this.props.groupId);
|
const group = this._matrixClient.getGroup(this.props.groupId);
|
||||||
if (!group) return null;
|
|
||||||
|
|
||||||
if (group.myMembership === 'invite') {
|
if (group && group.myMembership === 'invite') {
|
||||||
if (this.state.membershipBusy || this.state.inviterProfileBusy) {
|
if (this.state.membershipBusy || this.state.inviterProfileBusy) {
|
||||||
return <div className="mx_GroupView_membershipSection">
|
return <div className="mx_GroupView_membershipSection">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -874,33 +964,107 @@ export default React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (group.myMembership === 'join' && this.state.editing) {
|
}
|
||||||
const leaveButtonTooltip = this.state.isUserPrivileged ?
|
|
||||||
|
let membershipContainerExtraClasses;
|
||||||
|
let membershipButtonExtraClasses;
|
||||||
|
let membershipButtonTooltip;
|
||||||
|
let membershipButtonText;
|
||||||
|
let membershipButtonOnClick;
|
||||||
|
|
||||||
|
// User is not in the group
|
||||||
|
if ((!group || group.myMembership === 'leave') &&
|
||||||
|
this.state.summary &&
|
||||||
|
this.state.summary.profile &&
|
||||||
|
Boolean(this.state.summary.profile.is_openly_joinable)
|
||||||
|
) {
|
||||||
|
membershipButtonText = _t("Join this community");
|
||||||
|
membershipButtonOnClick = this._onJoinClick;
|
||||||
|
|
||||||
|
membershipButtonExtraClasses = 'mx_GroupView_joinButton';
|
||||||
|
membershipContainerExtraClasses = 'mx_GroupView_membershipSection_leave';
|
||||||
|
} else if (
|
||||||
|
group &&
|
||||||
|
group.myMembership === 'join' &&
|
||||||
|
this.state.editing
|
||||||
|
) {
|
||||||
|
membershipButtonText = _t("Leave this community");
|
||||||
|
membershipButtonOnClick = this._onLeaveClick;
|
||||||
|
membershipButtonTooltip = this.state.isUserPrivileged ?
|
||||||
_t("You are an administrator of this community") :
|
_t("You are an administrator of this community") :
|
||||||
_t("You are a member of this community");
|
_t("You are a member of this community");
|
||||||
const leaveButtonClasses = classnames({
|
|
||||||
"mx_RoomHeader_textButton": true,
|
membershipButtonExtraClasses = {
|
||||||
"mx_GroupView_textButton": true,
|
'mx_GroupView_leaveButton': true,
|
||||||
"mx_GroupView_leaveButton": true,
|
'mx_RoomHeader_textButton_danger': this.state.isUserPrivileged,
|
||||||
"mx_RoomHeader_textButton_danger": this.state.isUserPrivileged,
|
};
|
||||||
});
|
membershipContainerExtraClasses = 'mx_GroupView_membershipSection_joined';
|
||||||
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
|
} else {
|
||||||
<div className="mx_GroupView_membershipSubSection">
|
return null;
|
||||||
{ /* Empty div for flex alignment */ }
|
|
||||||
<div />
|
|
||||||
<div className="mx_GroupView_membership_buttonContainer">
|
|
||||||
<AccessibleButton
|
|
||||||
className={leaveButtonClasses}
|
|
||||||
onClick={this._onLeaveClick}
|
|
||||||
title={leaveButtonTooltip}
|
|
||||||
>
|
|
||||||
{ _t("Leave") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
const membershipButtonClasses = classnames([
|
||||||
|
'mx_RoomHeader_textButton',
|
||||||
|
'mx_GroupView_textButton',
|
||||||
|
],
|
||||||
|
membershipButtonExtraClasses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const membershipContainerClasses = classnames(
|
||||||
|
'mx_GroupView_membershipSection',
|
||||||
|
membershipContainerExtraClasses,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <div className={membershipContainerClasses}>
|
||||||
|
<div className="mx_GroupView_membershipSubSection">
|
||||||
|
{ /* The <div /> is for flex alignment */ }
|
||||||
|
{ this.state.membershipBusy ? <Spinner /> : <div /> }
|
||||||
|
<div className="mx_GroupView_membership_buttonContainer">
|
||||||
|
<AccessibleButton
|
||||||
|
className={membershipButtonClasses}
|
||||||
|
onClick={membershipButtonOnClick}
|
||||||
|
title={membershipButtonTooltip}
|
||||||
|
>
|
||||||
|
{ membershipButtonText }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getJoinableNode: function() {
|
||||||
|
return this.state.editing ? <div>
|
||||||
|
<h3>
|
||||||
|
{ _t('Who can join this community?') }
|
||||||
|
{ this.state.groupJoinableLoading ?
|
||||||
|
<InlineSpinner /> : <div />
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="radio"
|
||||||
|
value={GROUP_JOINPOLICY_INVITE}
|
||||||
|
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
|
||||||
|
onClick={this._onJoinableChange}
|
||||||
|
/>
|
||||||
|
<div className="mx_GroupView_label_text">
|
||||||
|
{ _t('Only people who have been invited') }
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="radio"
|
||||||
|
value={GROUP_JOINPOLICY_OPEN}
|
||||||
|
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
|
||||||
|
onClick={this._onJoinableChange}
|
||||||
|
/>
|
||||||
|
<div className="mx_GroupView_label_text">
|
||||||
|
{ _t('Everyone') }
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div> : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getLongDescriptionNode: function() {
|
_getLongDescriptionNode: function() {
|
||||||
|
@ -946,6 +1110,7 @@ export default React.createClass({
|
||||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
|
|
||||||
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
|
@ -1096,9 +1261,9 @@ export default React.createClass({
|
||||||
{ rightButtons }
|
{ rightButtons }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GeminiScrollbar className="mx_GroupView_body">
|
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||||
{ bodyNodes }
|
{ bodyNodes }
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbarWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk';
|
||||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||||
|
|
||||||
|
@ -26,18 +27,18 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// matrix client to use for UI auth requests
|
// matrix client to use for UI auth requests
|
||||||
matrixClient: React.PropTypes.object.isRequired,
|
matrixClient: PropTypes.object.isRequired,
|
||||||
|
|
||||||
// response from initial request. If not supplied, will do a request on
|
// response from initial request. If not supplied, will do a request on
|
||||||
// mount.
|
// mount.
|
||||||
authData: React.PropTypes.shape({
|
authData: PropTypes.shape({
|
||||||
flows: React.PropTypes.array,
|
flows: PropTypes.array,
|
||||||
params: React.PropTypes.object,
|
params: PropTypes.object,
|
||||||
session: React.PropTypes.string,
|
session: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// callback
|
// callback
|
||||||
makeRequest: React.PropTypes.func.isRequired,
|
makeRequest: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// callback called when the auth process has finished,
|
// callback called when the auth process has finished,
|
||||||
// successfully or unsuccessfully.
|
// successfully or unsuccessfully.
|
||||||
|
@ -51,22 +52,22 @@ export default React.createClass({
|
||||||
// the auth session.
|
// the auth session.
|
||||||
// * clientSecret {string} The client secret used in auth
|
// * clientSecret {string} The client secret used in auth
|
||||||
// sessions with the ID server.
|
// sessions with the ID server.
|
||||||
onAuthFinished: React.PropTypes.func.isRequired,
|
onAuthFinished: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// Inputs provided by the user to the auth process
|
// Inputs provided by the user to the auth process
|
||||||
// and used by various stages. As passed to js-sdk
|
// and used by various stages. As passed to js-sdk
|
||||||
// interactive-auth
|
// interactive-auth
|
||||||
inputs: React.PropTypes.object,
|
inputs: PropTypes.object,
|
||||||
|
|
||||||
// As js-sdk interactive-auth
|
// As js-sdk interactive-auth
|
||||||
makeRegistrationUrl: React.PropTypes.func,
|
makeRegistrationUrl: PropTypes.func,
|
||||||
sessionId: React.PropTypes.string,
|
sessionId: PropTypes.string,
|
||||||
clientSecret: React.PropTypes.string,
|
clientSecret: PropTypes.string,
|
||||||
emailSid: React.PropTypes.string,
|
emailSid: PropTypes.string,
|
||||||
|
|
||||||
// If true, poll to see if the auth flow has been completed
|
// If true, poll to see if the auth flow has been completed
|
||||||
// out-of-band
|
// out-of-band
|
||||||
poll: React.PropTypes.bool,
|
poll: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
||||||
|
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DragDropContext } from 'react-dnd';
|
import PropTypes from 'prop-types';
|
||||||
import HTML5Backend from 'react-dnd-html5-backend';
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
|
@ -31,6 +31,9 @@ import sessionStore from '../../stores/SessionStore';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
|
import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
|
import RoomListActions from '../../actions/RoomListActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||||
* determined by the page_type property.
|
* determined by the page_type property.
|
||||||
|
@ -44,23 +47,23 @@ const LoggedInView = React.createClass({
|
||||||
displayName: 'LoggedInView',
|
displayName: 'LoggedInView',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
page_type: React.PropTypes.string.isRequired,
|
page_type: PropTypes.string.isRequired,
|
||||||
onRoomCreated: React.PropTypes.func,
|
onRoomCreated: PropTypes.func,
|
||||||
onUserSettingsClose: React.PropTypes.func,
|
onUserSettingsClose: PropTypes.func,
|
||||||
|
|
||||||
// Called with the credentials of a registered user (if they were a ROU that
|
// Called with the credentials of a registered user (if they were a ROU that
|
||||||
// transitioned to PWLU)
|
// transitioned to PWLU)
|
||||||
onRegistered: React.PropTypes.func,
|
onRegistered: PropTypes.func,
|
||||||
|
|
||||||
teamToken: React.PropTypes.string,
|
teamToken: PropTypes.string,
|
||||||
|
|
||||||
// and lots and lots of other stuff.
|
// and lots and lots of other stuff.
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
|
||||||
authCache: React.PropTypes.object,
|
authCache: PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
getChildContext: function() {
|
getChildContext: function() {
|
||||||
|
@ -208,8 +211,51 @@ const LoggedInView = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onDragEnd: function(result) {
|
||||||
|
// Dragged to an invalid destination, not onto a droppable
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = result.destination.droppableId;
|
||||||
|
|
||||||
|
if (dest === 'tag-panel-droppable') {
|
||||||
|
// Could be "GroupTile +groupId:domain"
|
||||||
|
const draggableId = result.draggableId.split(' ').pop();
|
||||||
|
|
||||||
|
// Dispatch synchronously so that the TagPanel receives an
|
||||||
|
// optimistic update from TagOrderStore before the previous
|
||||||
|
// state is shown.
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(
|
||||||
|
this._matrixClient,
|
||||||
|
draggableId,
|
||||||
|
result.destination.index,
|
||||||
|
), true);
|
||||||
|
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||||
|
this._onRoomTileEndDrag(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRoomTileEndDrag: function(result) {
|
||||||
|
let newTag = result.destination.droppableId.split('_')[1];
|
||||||
|
let prevTag = result.source.droppableId.split('_')[1];
|
||||||
|
if (newTag === 'undefined') newTag = undefined;
|
||||||
|
if (prevTag === 'undefined') prevTag = undefined;
|
||||||
|
|
||||||
|
const roomId = result.draggableId.split('_')[1];
|
||||||
|
|
||||||
|
const oldIndex = result.source.index;
|
||||||
|
const newIndex = result.destination.index;
|
||||||
|
|
||||||
|
dis.dispatch(RoomListActions.tagRoom(
|
||||||
|
this._matrixClient,
|
||||||
|
this._matrixClient.getRoom(roomId),
|
||||||
|
prevTag, newTag,
|
||||||
|
oldIndex, newIndex,
|
||||||
|
), true);
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
|
||||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
const RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
|
@ -328,23 +374,23 @@ const LoggedInView = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
|
||||||
{ topBar }
|
{ topBar }
|
||||||
<div className={bodyClasses}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
<div className={bodyClasses}>
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
selectedRoom={this.props.currentRoomId}
|
collapsed={this.props.collapseLhs || false}
|
||||||
collapsed={this.props.collapseLhs || false}
|
disabled={this.props.leftDisabled}
|
||||||
disabled={this.props.leftDisabled}
|
/>
|
||||||
/>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
<main className='mx_MatrixChat_middlePanel'>
|
{ page_element }
|
||||||
{ page_element }
|
</main>
|
||||||
</main>
|
{ right_panel }
|
||||||
{ right_panel }
|
</div>
|
||||||
</div>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default DragDropContext(HTML5Backend)(LoggedInView);
|
export default LoggedInView;
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
|
|
||||||
import Analytics from "../../Analytics";
|
import Analytics from "../../Analytics";
|
||||||
|
@ -92,38 +93,38 @@ export default React.createClass({
|
||||||
displayName: 'MatrixChat',
|
displayName: 'MatrixChat',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
config: React.PropTypes.object,
|
config: PropTypes.object,
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: PropTypes.any,
|
||||||
onNewScreen: React.PropTypes.func,
|
onNewScreen: PropTypes.func,
|
||||||
registrationUrl: React.PropTypes.string,
|
registrationUrl: PropTypes.string,
|
||||||
enableGuest: React.PropTypes.bool,
|
enableGuest: PropTypes.bool,
|
||||||
|
|
||||||
// the queryParams extracted from the [real] query-string of the URI
|
// the queryParams extracted from the [real] query-string of the URI
|
||||||
realQueryParams: React.PropTypes.object,
|
realQueryParams: PropTypes.object,
|
||||||
|
|
||||||
// the initial queryParams extracted from the hash-fragment of the URI
|
// the initial queryParams extracted from the hash-fragment of the URI
|
||||||
startingFragmentQueryParams: React.PropTypes.object,
|
startingFragmentQueryParams: PropTypes.object,
|
||||||
|
|
||||||
// called when we have completed a token login
|
// called when we have completed a token login
|
||||||
onTokenLoginCompleted: React.PropTypes.func,
|
onTokenLoginCompleted: PropTypes.func,
|
||||||
|
|
||||||
// Represents the screen to display as a result of parsing the initial
|
// Represents the screen to display as a result of parsing the initial
|
||||||
// window.location
|
// window.location
|
||||||
initialScreenAfterLogin: React.PropTypes.shape({
|
initialScreenAfterLogin: PropTypes.shape({
|
||||||
screen: React.PropTypes.string.isRequired,
|
screen: PropTypes.string.isRequired,
|
||||||
params: React.PropTypes.object,
|
params: PropTypes.object,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// displayname, if any, to set on the device when logging
|
// displayname, if any, to set on the device when logging
|
||||||
// in/registering.
|
// in/registering.
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: PropTypes.string,
|
||||||
|
|
||||||
// A function that makes a registration URL
|
// A function that makes a registration URL
|
||||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
appConfig: React.PropTypes.object,
|
appConfig: PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
AuxPanel: {
|
AuxPanel: {
|
||||||
|
@ -170,6 +171,10 @@ export default React.createClass({
|
||||||
register_hs_url: null,
|
register_hs_url: null,
|
||||||
register_is_url: null,
|
register_is_url: null,
|
||||||
register_id_sid: null,
|
register_id_sid: null,
|
||||||
|
|
||||||
|
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||||
|
// and disable it when there are no dialogs
|
||||||
|
hideToSRUsers: false,
|
||||||
};
|
};
|
||||||
return s;
|
return s;
|
||||||
},
|
},
|
||||||
|
@ -286,6 +291,8 @@ export default React.createClass({
|
||||||
this.handleResize();
|
this.handleResize();
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
|
||||||
|
this._pageChanging = false;
|
||||||
|
|
||||||
// check we have the right tint applied for this theme.
|
// check we have the right tint applied for this theme.
|
||||||
// N.B. we don't call the whole of setTheme() here as we may be
|
// N.B. we don't call the whole of setTheme() here as we may be
|
||||||
// racing with the theme CSS download finishing from index.js
|
// racing with the theme CSS download finishing from index.js
|
||||||
|
@ -363,13 +370,58 @@ export default React.createClass({
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentWillUpdate: function(props, state) {
|
||||||
|
if (this.shouldTrackPageChange(this.state, state)) {
|
||||||
|
this.startPageChangeTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function(prevProps, prevState) {
|
||||||
|
if (this.shouldTrackPageChange(prevState, this.state)) {
|
||||||
|
const durationMs = this.stopPageChangeTimer();
|
||||||
|
Analytics.trackPageChange(durationMs);
|
||||||
|
}
|
||||||
if (this.focusComposer) {
|
if (this.focusComposer) {
|
||||||
dis.dispatch({action: 'focus_composer'});
|
dis.dispatch({action: 'focus_composer'});
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startPageChangeTimer() {
|
||||||
|
// This shouldn't happen because componentWillUpdate and componentDidUpdate
|
||||||
|
// are used.
|
||||||
|
if (this._pageChanging) {
|
||||||
|
console.warn('MatrixChat.startPageChangeTimer: timer already started');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._pageChanging = true;
|
||||||
|
performance.mark('riot_MatrixChat_page_change_start');
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPageChangeTimer() {
|
||||||
|
if (!this._pageChanging) {
|
||||||
|
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._pageChanging = false;
|
||||||
|
performance.mark('riot_MatrixChat_page_change_stop');
|
||||||
|
performance.measure(
|
||||||
|
'riot_MatrixChat_page_change_delta',
|
||||||
|
'riot_MatrixChat_page_change_start',
|
||||||
|
'riot_MatrixChat_page_change_stop',
|
||||||
|
);
|
||||||
|
performance.clearMarks('riot_MatrixChat_page_change_start');
|
||||||
|
performance.clearMarks('riot_MatrixChat_page_change_stop');
|
||||||
|
const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop();
|
||||||
|
return measurement.duration;
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldTrackPageChange(prevState, state) {
|
||||||
|
return prevState.currentRoomId !== state.currentRoomId ||
|
||||||
|
prevState.view !== state.view ||
|
||||||
|
prevState.page_type !== state.page_type;
|
||||||
|
},
|
||||||
|
|
||||||
setStateForNewView: function(state) {
|
setStateForNewView: function(state) {
|
||||||
if (state.view === undefined) {
|
if (state.view === undefined) {
|
||||||
throw new Error("setStateForNewView with no view!");
|
throw new Error("setStateForNewView with no view!");
|
||||||
|
@ -607,6 +659,16 @@ export default React.createClass({
|
||||||
case 'send_event':
|
case 'send_event':
|
||||||
this.onSendEvent(payload.room_id, payload.event);
|
this.onSendEvent(payload.room_id, payload.event);
|
||||||
break;
|
break;
|
||||||
|
case 'aria_hide_main_app':
|
||||||
|
this.setState({
|
||||||
|
hideToSRUsers: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'aria_unhide_main_app':
|
||||||
|
this.setState({
|
||||||
|
hideToSRUsers: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -617,18 +679,26 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_startRegistration: function(params) {
|
_startRegistration: function(params) {
|
||||||
this.setStateForNewView({
|
const newState = {
|
||||||
view: VIEWS.REGISTER,
|
view: VIEWS.REGISTER,
|
||||||
// these params may be undefined, but if they are,
|
};
|
||||||
// unset them from our state: we don't want to
|
|
||||||
// resume a previous registration session if the
|
// Only honour params if they are all present, otherwise we reset
|
||||||
// user just clicked 'register'
|
// HS and IS URLs when switching to registration.
|
||||||
register_client_secret: params.client_secret,
|
if (params.client_secret &&
|
||||||
register_session_id: params.session_id,
|
params.session_id &&
|
||||||
register_hs_url: params.hs_url,
|
params.hs_url &&
|
||||||
register_is_url: params.is_url,
|
params.is_url &&
|
||||||
register_id_sid: params.sid,
|
params.sid
|
||||||
});
|
) {
|
||||||
|
newState.register_client_secret = params.client_secret;
|
||||||
|
newState.register_session_id = params.session_id;
|
||||||
|
newState.register_hs_url = params.hs_url;
|
||||||
|
newState.register_is_url = params.is_url;
|
||||||
|
newState.register_id_sid = params.sid;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStateForNewView(newState);
|
||||||
this.notifyNewScreen('register');
|
this.notifyNewScreen('register');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -846,16 +916,36 @@ export default React.createClass({
|
||||||
}).close;
|
}).close;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_leaveRoomWarnings: function(roomId) {
|
||||||
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
// Show a warning if there are additional complications.
|
||||||
|
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
|
||||||
|
const warnings = [];
|
||||||
|
if (joinRules) {
|
||||||
|
const rule = joinRules.getContent().join_rule;
|
||||||
|
if (rule !== "public") {
|
||||||
|
warnings.push((
|
||||||
|
<span className="warning" key="non_public_warning">
|
||||||
|
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
},
|
||||||
|
|
||||||
_leaveRoom: function(roomId) {
|
_leaveRoom: function(roomId) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
const warnings = this._leaveRoomWarnings(roomId);
|
||||||
|
|
||||||
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
||||||
title: _t("Leave room"),
|
title: _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
<span>
|
<span>
|
||||||
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||||
|
{ warnings }
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onFinished: (shouldLeave) => {
|
onFinished: (shouldLeave) => {
|
||||||
|
@ -1065,10 +1155,10 @@ export default React.createClass({
|
||||||
// this if we are not scrolled up in the view. To find out, delegate to
|
// this if we are not scrolled up in the view. To find out, delegate to
|
||||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||||
// it is safe to reset the timeline.
|
// it is safe to reset the timeline.
|
||||||
if (!self._loggedInView) {
|
if (!self._loggedInView || !self._loggedInView.child) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId);
|
return self._loggedInView.child.canResetTimelineInRoom(roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
cli.on('sync', function(state, prevState) {
|
cli.on('sync', function(state, prevState) {
|
||||||
|
@ -1142,18 +1232,6 @@ export default React.createClass({
|
||||||
cli.on("crypto.warning", (type) => {
|
cli.on("crypto.warning", (type) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'CRYPTO_WARNING_ACCOUNT_MIGRATED':
|
|
||||||
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
|
|
||||||
title: _t('Cryptography data migrated'),
|
|
||||||
description: _t(
|
|
||||||
"A one-off migration of cryptography data has been performed. "+
|
|
||||||
"End-to-end encryption will not work if you go back to an older "+
|
|
||||||
"version of Riot. If you need to use end-to-end cryptography on "+
|
|
||||||
"an older version, log out of Riot first. To retain message history, "+
|
|
||||||
"export and re-import your keys.",
|
|
||||||
),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
|
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
|
||||||
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
|
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
|
||||||
title: _t('Old cryptography data detected'),
|
title: _t('Old cryptography data detected'),
|
||||||
|
@ -1310,7 +1388,6 @@ export default React.createClass({
|
||||||
if (this.props.onNewScreen) {
|
if (this.props.onNewScreen) {
|
||||||
this.props.onNewScreen(screen);
|
this.props.onNewScreen(screen);
|
||||||
}
|
}
|
||||||
Analytics.trackPageChange();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onAliasClick: function(event, alias) {
|
onAliasClick: function(event, alias) {
|
||||||
|
@ -1480,6 +1557,17 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onServerConfigChange(config) {
|
||||||
|
const newState = {};
|
||||||
|
if (config.hsUrl) {
|
||||||
|
newState.register_hs_url = config.hsUrl;
|
||||||
|
}
|
||||||
|
if (config.isUrl) {
|
||||||
|
newState.register_is_url = config.isUrl;
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
|
},
|
||||||
|
|
||||||
_makeRegistrationUrl: function(params) {
|
_makeRegistrationUrl: function(params) {
|
||||||
if (this.props.startingFragmentQueryParams.referrer) {
|
if (this.props.startingFragmentQueryParams.referrer) {
|
||||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||||
|
@ -1568,6 +1656,7 @@ export default React.createClass({
|
||||||
onLoginClick={this.onLoginClick}
|
onLoginClick={this.onLoginClick}
|
||||||
onRegisterClick={this.onRegisterClick}
|
onRegisterClick={this.onRegisterClick}
|
||||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||||
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1602,6 +1691,7 @@ export default React.createClass({
|
||||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||||
enableGuest={this.props.enableGuest}
|
enableGuest={this.props.enableGuest}
|
||||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||||
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
|
import {wantsDateSeparator} from '../../DateUtils';
|
||||||
import dis from "../../dispatcher";
|
import dis from "../../dispatcher";
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
|
||||||
const MILLIS_IN_DAY = 86400000;
|
|
||||||
|
|
||||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -32,63 +32,63 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// true to give the component a 'display: none' style.
|
// true to give the component a 'display: none' style.
|
||||||
hidden: React.PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
|
||||||
// true to show a spinner at the top of the timeline to indicate
|
// true to show a spinner at the top of the timeline to indicate
|
||||||
// back-pagination in progress
|
// back-pagination in progress
|
||||||
backPaginating: React.PropTypes.bool,
|
backPaginating: PropTypes.bool,
|
||||||
|
|
||||||
// true to show a spinner at the end of the timeline to indicate
|
// true to show a spinner at the end of the timeline to indicate
|
||||||
// forward-pagination in progress
|
// forward-pagination in progress
|
||||||
forwardPaginating: React.PropTypes.bool,
|
forwardPaginating: PropTypes.bool,
|
||||||
|
|
||||||
// the list of MatrixEvents to display
|
// the list of MatrixEvents to display
|
||||||
events: React.PropTypes.array.isRequired,
|
events: PropTypes.array.isRequired,
|
||||||
|
|
||||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||||
highlightedEventId: React.PropTypes.string,
|
highlightedEventId: PropTypes.string,
|
||||||
|
|
||||||
// Should we show URL Previews
|
// Should we show URL Previews
|
||||||
showUrlPreview: React.PropTypes.bool,
|
showUrlPreview: PropTypes.bool,
|
||||||
|
|
||||||
// event after which we should show a read marker
|
// event after which we should show a read marker
|
||||||
readMarkerEventId: React.PropTypes.string,
|
readMarkerEventId: PropTypes.string,
|
||||||
|
|
||||||
// whether the read marker should be visible
|
// whether the read marker should be visible
|
||||||
readMarkerVisible: React.PropTypes.bool,
|
readMarkerVisible: PropTypes.bool,
|
||||||
|
|
||||||
// the userid of our user. This is used to suppress the read marker
|
// the userid of our user. This is used to suppress the read marker
|
||||||
// for pending messages.
|
// for pending messages.
|
||||||
ourUserId: React.PropTypes.string,
|
ourUserId: PropTypes.string,
|
||||||
|
|
||||||
// true to suppress the date at the start of the timeline
|
// true to suppress the date at the start of the timeline
|
||||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
suppressFirstDateSeparator: PropTypes.bool,
|
||||||
|
|
||||||
// whether to show read receipts
|
// whether to show read receipts
|
||||||
showReadReceipts: React.PropTypes.bool,
|
showReadReceipts: PropTypes.bool,
|
||||||
|
|
||||||
// true if updates to the event list should cause the scroll panel to
|
// true if updates to the event list should cause the scroll panel to
|
||||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||||
// for more details.
|
// for more details.
|
||||||
stickyBottom: React.PropTypes.bool,
|
stickyBottom: PropTypes.bool,
|
||||||
|
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when more content is needed.
|
// callback which is called when more content is needed.
|
||||||
onFillRequest: React.PropTypes.func,
|
onFillRequest: PropTypes.func,
|
||||||
|
|
||||||
// className for the panel
|
// className for the panel
|
||||||
className: React.PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// shape parameter to be passed to EventTiles
|
// shape parameter to be passed to EventTiles
|
||||||
tileShape: React.PropTypes.string,
|
tileShape: PropTypes.string,
|
||||||
|
|
||||||
// show twelve hour timestamps
|
// show twelve hour timestamps
|
||||||
isTwelveHour: React.PropTypes.bool,
|
isTwelveHour: PropTypes.bool,
|
||||||
|
|
||||||
// show timestamps always
|
// show timestamps always
|
||||||
alwaysShowTimestamps: React.PropTypes.bool,
|
alwaysShowTimestamps: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -325,7 +325,7 @@ module.exports = React.createClass({
|
||||||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||||
|
|
||||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,10 +447,18 @@ module.exports = React.createClass({
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
let continuation = false;
|
let continuation = false;
|
||||||
|
|
||||||
|
// Some events should appear as continuations from previous events of
|
||||||
|
// different types.
|
||||||
|
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||||
|
const eventTypeContinues =
|
||||||
|
prevEvent !== null &&
|
||||||
|
continuedTypes.includes(mxEv.getType()) &&
|
||||||
|
continuedTypes.includes(prevEvent.getType());
|
||||||
|
|
||||||
if (prevEvent !== null
|
if (prevEvent !== null
|
||||||
&& prevEvent.sender && mxEv.sender
|
&& prevEvent.sender && mxEv.sender
|
||||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||||
&& mxEv.getType() == prevEvent.getType()) {
|
&& (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) {
|
||||||
continuation = true;
|
continuation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,7 +487,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// do we need a date separator since the last event?
|
// do we need a date separator since the last event?
|
||||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
continuation = false;
|
continuation = false;
|
||||||
}
|
}
|
||||||
|
@ -522,17 +530,7 @@ module.exports = React.createClass({
|
||||||
// here.
|
// here.
|
||||||
return !this.props.suppressFirstDateSeparator;
|
return !this.props.suppressFirstDateSeparator;
|
||||||
}
|
}
|
||||||
const prevEventDate = prevEvent.getDate();
|
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||||
if (!nextEventDate || !prevEventDate) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Return early for events that are > 24h apart
|
|
||||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare weekdays
|
|
||||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// get a list of read receipts that should be shown next to this event
|
// get a list of read receipts that should be shown next to this event
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
@ -26,7 +26,7 @@ export default withMatrixClient(React.createClass({
|
||||||
displayName: 'MyGroups',
|
displayName: 'MyGroups',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: React.PropTypes.object.isRequired,
|
matrixClient: PropTypes.object.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -62,6 +62,8 @@ export default withMatrixClient(React.createClass({
|
||||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
const GroupTile = sdk.getComponent("groups.GroupTile");
|
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||||
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
|
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
let contentHeader;
|
let contentHeader;
|
||||||
|
@ -72,9 +74,26 @@ export default withMatrixClient(React.createClass({
|
||||||
});
|
});
|
||||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||||
content = groupNodes.length > 0 ?
|
content = groupNodes.length > 0 ?
|
||||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
<GeminiScrollbarWrapper>
|
||||||
{ groupNodes }
|
<div className="mx_MyGroups_microcopy">
|
||||||
</GeminiScrollbar> :
|
<p>
|
||||||
|
{ _t(
|
||||||
|
"Did you know: you can use communities to filter your Riot.im experience!",
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
"To set up a filter, drag a community avatar over to the filter panel on " +
|
||||||
|
"the far left hand side of the screen. You can click on an avatar in the " +
|
||||||
|
"filter panel at any time to see only the rooms and people associated " +
|
||||||
|
"with that community.",
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mx_MyGroups_joinedGroups">
|
||||||
|
{ groupNodes }
|
||||||
|
</div>
|
||||||
|
</GeminiScrollbarWrapper> :
|
||||||
<div className="mx_MyGroups_placeholder">
|
<div className="mx_MyGroups_placeholder">
|
||||||
{ _t(
|
{ _t(
|
||||||
"You're not currently a member of any communities.",
|
"You're not currently a member of any communities.",
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
@ -23,7 +24,7 @@ import WhoIsTyping from '../../WhoIsTyping';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
|
import * as cryptodevices from '../../cryptodevices';
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
|
@ -41,59 +42,59 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: React.PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
|
|
||||||
// the number of messages which have arrived since we've been scrolled up
|
// the number of messages which have arrived since we've been scrolled up
|
||||||
numUnreadMessages: React.PropTypes.number,
|
numUnreadMessages: PropTypes.number,
|
||||||
|
|
||||||
// this is true if we are fully scrolled-down, and are looking at
|
// this is true if we are fully scrolled-down, and are looking at
|
||||||
// the end of the live timeline.
|
// the end of the live timeline.
|
||||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
atEndOfLiveTimeline: PropTypes.bool,
|
||||||
|
|
||||||
// This is true when the user is alone in the room, but has also sent a message.
|
// This is true when the user is alone in the room, but has also sent a message.
|
||||||
// Used to suggest to the user to invite someone
|
// Used to suggest to the user to invite someone
|
||||||
sentMessageAndIsAlone: React.PropTypes.bool,
|
sentMessageAndIsAlone: PropTypes.bool,
|
||||||
|
|
||||||
// true if there is an active call in this room (means we show
|
// true if there is an active call in this room (means we show
|
||||||
// the 'Active Call' text in the status bar if there is nothing
|
// the 'Active Call' text in the status bar if there is nothing
|
||||||
// more interesting)
|
// more interesting)
|
||||||
hasActiveCall: React.PropTypes.bool,
|
hasActiveCall: PropTypes.bool,
|
||||||
|
|
||||||
// Number of names to display in typing indication. E.g. set to 3, will
|
// Number of names to display in typing indication. E.g. set to 3, will
|
||||||
// result in "X, Y, Z and 100 others are typing."
|
// result in "X, Y, Z and 100 others are typing."
|
||||||
whoIsTypingLimit: React.PropTypes.number,
|
whoIsTypingLimit: PropTypes.number,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'resend all' button in the
|
// callback for when the user clicks on the 'resend all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onResendAllClick: React.PropTypes.func,
|
onResendAllClick: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'cancel all' button in the
|
// callback for when the user clicks on the 'cancel all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onCancelAllClick: React.PropTypes.func,
|
onCancelAllClick: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'invite others' button in the
|
// callback for when the user clicks on the 'invite others' button in the
|
||||||
// 'you are alone' bar
|
// 'you are alone' bar
|
||||||
onInviteClick: React.PropTypes.func,
|
onInviteClick: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'stop warning me' button in the
|
// callback for when the user clicks on the 'stop warning me' button in the
|
||||||
// 'you are alone' bar
|
// 'you are alone' bar
|
||||||
onStopWarningClick: React.PropTypes.func,
|
onStopWarningClick: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'scroll to bottom' button
|
// callback for when the user clicks on the 'scroll to bottom' button
|
||||||
onScrollToBottomClick: React.PropTypes.func,
|
onScrollToBottomClick: PropTypes.func,
|
||||||
|
|
||||||
// callback for when we do something that changes the size of the
|
// callback for when we do something that changes the size of the
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
// component.
|
// component.
|
||||||
onResize: React.PropTypes.func,
|
onResize: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the status bar can be hidden from view, as it is
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
// not displaying anything
|
// not displaying anything
|
||||||
onHidden: React.PropTypes.func,
|
onHidden: PropTypes.func,
|
||||||
|
|
||||||
// callback for when the status bar is displaying something and should
|
// callback for when the status bar is displaying something and should
|
||||||
// be visible
|
// be visible
|
||||||
onVisible: React.PropTypes.func,
|
onVisible: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -147,6 +148,13 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onSendWithoutVerifyingClick: function() {
|
||||||
|
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||||
|
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||||
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_onResendAllClick: function() {
|
_onResendAllClick: function() {
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
},
|
},
|
||||||
|
@ -156,7 +164,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onShowDevicesClick: function() {
|
_onShowDevicesClick: function() {
|
||||||
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||||
},
|
},
|
||||||
|
|
||||||
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||||
|
@ -169,8 +177,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||||
_checkSize: function() {
|
_checkSize: function() {
|
||||||
if (this.props.onVisible && this._getSize()) {
|
if (this._getSize()) {
|
||||||
this.props.onVisible();
|
if (this.props.onVisible) this.props.onVisible();
|
||||||
|
} else {
|
||||||
|
if (this.props.onHidden) this.props.onHidden();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -286,10 +296,11 @@ module.exports = React.createClass({
|
||||||
if (hasUDE) {
|
if (hasUDE) {
|
||||||
title = _t("Message not sent due to unknown devices being present");
|
title = _t("Message not sent due to unknown devices being present");
|
||||||
content = _t(
|
content = _t(
|
||||||
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
|
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||||
|
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
|
||||||
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -302,11 +313,11 @@ module.exports = React.createClass({
|
||||||
) {
|
) {
|
||||||
title = unsentMessages[0].error.data.error;
|
title = unsentMessages[0].error.data.error;
|
||||||
} else {
|
} else {
|
||||||
title = _t("Some of your messages have not been sent.");
|
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||||
}
|
}
|
||||||
content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||||
"You can also select individual messages to resend or cancel.",
|
"You can also select individual messages to resend or cancel.",
|
||||||
{},
|
{ count: unsentMessages.length },
|
||||||
{
|
{
|
||||||
'resendText': (sub) =>
|
'resendText': (sub) =>
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import shouldHideEvent from "../../shouldHideEvent";
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
@ -58,18 +59,18 @@ if (DEBUG) {
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomView',
|
displayName: 'RoomView',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: PropTypes.any,
|
||||||
|
|
||||||
// Called with the credentials of a registered user (if they were a ROU that
|
// Called with the credentials of a registered user (if they were a ROU that
|
||||||
// transitioned to PWLU)
|
// transitioned to PWLU)
|
||||||
onRegistered: React.PropTypes.func,
|
onRegistered: PropTypes.func,
|
||||||
|
|
||||||
// An object representing a third party invite to join this room
|
// An object representing a third party invite to join this room
|
||||||
// Fields:
|
// Fields:
|
||||||
// * inviteSignUrl (string) The URL used to join this room from an email invite
|
// * inviteSignUrl (string) The URL used to join this room from an email invite
|
||||||
// (given as part of the link in the invite email)
|
// (given as part of the link in the invite email)
|
||||||
// * invitedEmail (string) The email address that was invited to this room
|
// * invitedEmail (string) The email address that was invited to this room
|
||||||
thirdPartyInvite: React.PropTypes.object,
|
thirdPartyInvite: PropTypes.object,
|
||||||
|
|
||||||
// Any data about the room that would normally come from the Home Server
|
// Any data about the room that would normally come from the Home Server
|
||||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||||
|
@ -80,10 +81,10 @@ module.exports = React.createClass({
|
||||||
// * avatarUrl (string) The mxc:// avatar URL for the room
|
// * avatarUrl (string) The mxc:// avatar URL for the room
|
||||||
// * inviterName (string) The display name of the person who
|
// * inviterName (string) The display name of the person who
|
||||||
// * invited us tovthe room
|
// * invited us tovthe room
|
||||||
oobData: React.PropTypes.object,
|
oobData: PropTypes.object,
|
||||||
|
|
||||||
// is the RightPanel collapsed?
|
// is the RightPanel collapsed?
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -263,12 +264,19 @@ module.exports = React.createClass({
|
||||||
isPeeking: true, // this will change to false if peeking fails
|
isPeeking: true, // this will change to false if peeking fails
|
||||||
});
|
});
|
||||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room,
|
room: room,
|
||||||
peekLoading: false,
|
peekLoading: false,
|
||||||
});
|
});
|
||||||
this._onRoomLoaded(room);
|
this._onRoomLoaded(room);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Stop peeking if anything went wrong
|
// Stop peeking if anything went wrong
|
||||||
this.setState({
|
this.setState({
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
|
@ -285,7 +293,7 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}).done();
|
});
|
||||||
}
|
}
|
||||||
} else if (room) {
|
} else if (room) {
|
||||||
// Stop peeking because we have joined this room previously
|
// Stop peeking because we have joined this room previously
|
||||||
|
@ -459,6 +467,15 @@ module.exports = React.createClass({
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
this._checkIfAlone(this.state.room);
|
this._checkIfAlone(this.state.room);
|
||||||
break;
|
break;
|
||||||
|
case 'post_sticker_message':
|
||||||
|
this.injectSticker(
|
||||||
|
payload.data.content.url,
|
||||||
|
payload.data.content.info,
|
||||||
|
payload.data.description || payload.data.name);
|
||||||
|
break;
|
||||||
|
case 'picture_snapshot':
|
||||||
|
this.uploadFile(payload.file);
|
||||||
|
break;
|
||||||
case 'notifier_enabled':
|
case 'notifier_enabled':
|
||||||
case 'upload_failed':
|
case 'upload_failed':
|
||||||
case 'upload_started':
|
case 'upload_started':
|
||||||
|
@ -619,8 +636,8 @@ module.exports = React.createClass({
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
|
|
||||||
console.log("Tinter.tint from updateTint");
|
console.log("Tinter.tint from updateTint");
|
||||||
|
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -669,23 +686,7 @@ module.exports = React.createClass({
|
||||||
// a member state changed in this room
|
// a member state changed in this room
|
||||||
// refresh the conf call notification state
|
// refresh the conf call notification state
|
||||||
this._updateConfCallNotification();
|
this._updateConfCallNotification();
|
||||||
|
this._updateDMState();
|
||||||
// if we are now a member of the room, where we were not before, that
|
|
||||||
// means we have finished joining a room we were previously peeking
|
|
||||||
// into.
|
|
||||||
const me = MatrixClientPeg.get().credentials.userId;
|
|
||||||
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
|
|
||||||
// Having just joined a room, check to see if it looks like a DM room, and if so,
|
|
||||||
// mark it as one. This is to work around the fact that some clients don't support
|
|
||||||
// is_direct. We should remove this once they do.
|
|
||||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
|
||||||
if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) {
|
|
||||||
// XXX: There's not a whole lot we can really do if this fails: at best
|
|
||||||
// perhaps we could try a couple more times, but since it's a temporary
|
|
||||||
// compatability workaround, let's not bother.
|
|
||||||
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
_checkIfAlone: function(room) {
|
_checkIfAlone: function(room) {
|
||||||
|
@ -726,6 +727,44 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_updateDMState() {
|
||||||
|
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
if (!me || me.membership !== "join") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user may have accepted an invite with is_direct set
|
||||||
|
if (me.events.member.getPrevContent().membership === "invite" &&
|
||||||
|
me.events.member.getPrevContent().is_direct
|
||||||
|
) {
|
||||||
|
// This is a DM with the sender of the invite event (which we assume
|
||||||
|
// preceded the join event)
|
||||||
|
Rooms.setDMRoom(
|
||||||
|
this.state.room.roomId,
|
||||||
|
me.events.member.getUnsigned().prev_sender,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invitedMembers = this.state.room.getMembersWithMembership("invite");
|
||||||
|
const joinedMembers = this.state.room.getMembersWithMembership("join");
|
||||||
|
|
||||||
|
// There must be one invited member and one joined member
|
||||||
|
if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user may have sent an invite with is_direct sent
|
||||||
|
const other = invitedMembers[0];
|
||||||
|
if (other &&
|
||||||
|
other.membership === "invite" &&
|
||||||
|
other.events.member.getContent().is_direct
|
||||||
|
) {
|
||||||
|
Rooms.setDMRoom(this.state.room.roomId, other.userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onSearchResultsResize: function() {
|
onSearchResultsResize: function() {
|
||||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||||
},
|
},
|
||||||
|
@ -818,18 +857,6 @@ module.exports = React.createClass({
|
||||||
action: 'join_room',
|
action: 'join_room',
|
||||||
opts: { inviteSignUrl: signUrl },
|
opts: { inviteSignUrl: signUrl },
|
||||||
});
|
});
|
||||||
|
|
||||||
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
|
|
||||||
if (this.state.room) {
|
|
||||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
|
||||||
if (me && me.membership == 'invite') {
|
|
||||||
if (me.events.member.getContent().is_direct) {
|
|
||||||
// The 'direct' hint is there, so declare that this is a DM room for
|
|
||||||
// whoever invited us.
|
|
||||||
return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -889,7 +916,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
ContentMessages.sendContentToRoom(
|
ContentMessages.sendContentToRoom(
|
||||||
file, this.state.room.roomId, MatrixClientPeg.get(),
|
file, this.state.room.roomId, MatrixClientPeg.get(),
|
||||||
).done(undefined, (error) => {
|
).catch((error) => {
|
||||||
if (error.name === "UnknownDeviceError") {
|
if (error.name === "UnknownDeviceError") {
|
||||||
// Let the staus bar handle this
|
// Let the staus bar handle this
|
||||||
return;
|
return;
|
||||||
|
@ -898,11 +925,27 @@ module.exports = React.createClass({
|
||||||
console.error("Failed to upload file " + file + " " + error);
|
console.error("Failed to upload file " + file + " " + error);
|
||||||
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
||||||
title: _t('Failed to upload file'),
|
title: _t('Failed to upload file'),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
description: ((error && error.message)
|
||||||
|
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
injectSticker: function(url, info, text) {
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||||
|
.done(undefined, (error) => {
|
||||||
|
if (error.name === "UnknownDeviceError") {
|
||||||
|
// Let the staus bar handle this
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onSearch: function(term, scope) {
|
onSearch: function(term, scope) {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchTerm: term,
|
searchTerm: term,
|
||||||
|
@ -1347,10 +1390,12 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatusBarHidden: function() {
|
onStatusBarHidden: function() {
|
||||||
if (this.unmounted) return;
|
// This is currently not desired as it is annoying if it keeps expanding and collapsing
|
||||||
|
// TODO: Find a less annoying way of hiding the status bar
|
||||||
|
/*if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
statusBarVisible: false,
|
statusBarVisible: false,
|
||||||
});
|
});*/
|
||||||
},
|
},
|
||||||
|
|
||||||
showSettings: function(show) {
|
showSettings: function(show) {
|
||||||
|
@ -1583,7 +1628,8 @@ module.exports = React.createClass({
|
||||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
onResize={this.onChildResize}
|
onResize={this.onChildResize}
|
||||||
showApps={this.state.showApps && !this.state.editingRoomSettings} >
|
showApps={this.state.showApps}
|
||||||
|
hideAppsDrawer={this.state.editingRoomSettings} >
|
||||||
{ aux }
|
{ aux }
|
||||||
</AuxPanel>
|
</AuxPanel>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
import PropTypes from 'prop-types';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { KeyCode } from '../../Keyboard';
|
import { KeyCode } from '../../Keyboard';
|
||||||
|
import sdk from '../../index.js';
|
||||||
|
|
||||||
const DEBUG_SCROLL = false;
|
const DEBUG_SCROLL = false;
|
||||||
// var DEBUG_SCROLL = true;
|
// var DEBUG_SCROLL = true;
|
||||||
|
@ -86,7 +87,7 @@ module.exports = React.createClass({
|
||||||
* scroll down to show the new element, rather than preserving the
|
* scroll down to show the new element, rather than preserving the
|
||||||
* existing view.
|
* existing view.
|
||||||
*/
|
*/
|
||||||
stickyBottom: React.PropTypes.bool,
|
stickyBottom: PropTypes.bool,
|
||||||
|
|
||||||
/* startAtBottom: if set to true, the view is assumed to start
|
/* startAtBottom: if set to true, the view is assumed to start
|
||||||
* scrolled to the bottom.
|
* scrolled to the bottom.
|
||||||
|
@ -95,7 +96,7 @@ module.exports = React.createClass({
|
||||||
* behaviour stays the same for other uses of ScrollPanel.
|
* behaviour stays the same for other uses of ScrollPanel.
|
||||||
* If so, let's remove this parameter down the line.
|
* If so, let's remove this parameter down the line.
|
||||||
*/
|
*/
|
||||||
startAtBottom: React.PropTypes.bool,
|
startAtBottom: PropTypes.bool,
|
||||||
|
|
||||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||||
* the user nears the start (backwards = true) or end (backwards =
|
* the user nears the start (backwards = true) or end (backwards =
|
||||||
|
@ -110,7 +111,7 @@ module.exports = React.createClass({
|
||||||
* directon (at this time) - which will stop the pagination cycle until
|
* directon (at this time) - which will stop the pagination cycle until
|
||||||
* the user scrolls again.
|
* the user scrolls again.
|
||||||
*/
|
*/
|
||||||
onFillRequest: React.PropTypes.func,
|
onFillRequest: PropTypes.func,
|
||||||
|
|
||||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||||
* there are children elements that are far out of view and could be removed
|
* there are children elements that are far out of view and could be removed
|
||||||
|
@ -121,24 +122,24 @@ module.exports = React.createClass({
|
||||||
* first element to remove if removing from the front/bottom, and last element
|
* first element to remove if removing from the front/bottom, and last element
|
||||||
* to remove if removing from the back/top.
|
* to remove if removing from the back/top.
|
||||||
*/
|
*/
|
||||||
onUnfillRequest: React.PropTypes.func,
|
onUnfillRequest: PropTypes.func,
|
||||||
|
|
||||||
/* onScroll: a callback which is called whenever any scroll happens.
|
/* onScroll: a callback which is called whenever any scroll happens.
|
||||||
*/
|
*/
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
/* onResize: a callback which is called whenever the Gemini scroll
|
/* onResize: a callback which is called whenever the Gemini scroll
|
||||||
* panel is resized
|
* panel is resized
|
||||||
*/
|
*/
|
||||||
onResize: React.PropTypes.func,
|
onResize: PropTypes.func,
|
||||||
|
|
||||||
/* className: classnames to add to the top-level div
|
/* className: classnames to add to the top-level div
|
||||||
*/
|
*/
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
|
||||||
/* style: styles to add to the top-level div
|
/* style: styles to add to the top-level div
|
||||||
*/
|
*/
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -223,7 +224,7 @@ module.exports = React.createClass({
|
||||||
onResize: function() {
|
onResize: function() {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
this.checkScroll();
|
this.checkScroll();
|
||||||
this.refs.geminiPanel.forceUpdate();
|
if (this._gemScroll) this._gemScroll.forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
// after an update to the contents of the panel, check that the scroll is
|
// after an update to the contents of the panel, check that the scroll is
|
||||||
|
@ -664,14 +665,25 @@ module.exports = React.createClass({
|
||||||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.refs.geminiPanel.scrollbar.getViewElement();
|
if (!this._gemScroll) {
|
||||||
|
// Likewise, we should have the ref by this point, but if not
|
||||||
|
// turn the NPE into something meaningful.
|
||||||
|
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._gemScroll.scrollbar.getViewElement();
|
||||||
|
},
|
||||||
|
|
||||||
|
_collectGeminiScroll: function(gemScroll) {
|
||||||
|
this._gemScroll = gemScroll;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
// TODO: the classnames on the div and ol could do with being updated to
|
// TODO: the classnames on the div and ol could do with being updated to
|
||||||
// reflect the fact that we don't necessarily contain a list of messages.
|
// reflect the fact that we don't necessarily contain a list of messages.
|
||||||
// it's not obvious why we have a separate div and ol anyway.
|
// it's not obvious why we have a separate div and ol anyway.
|
||||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
return (<GeminiScrollbarWrapper autoshow={true} wrappedRef={this._collectGeminiScroll}
|
||||||
onScroll={this.onScroll} onResize={this.onResize}
|
onScroll={this.onScroll} onResize={this.onResize}
|
||||||
className={this.props.className} style={this.props.style}>
|
className={this.props.className} style={this.props.style}>
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
|
@ -679,7 +691,7 @@ module.exports = React.createClass({
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbarWrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,15 +17,15 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import FilterStore from '../../stores/FilterStore';
|
|
||||||
import FlairStore from '../../stores/FlairStore';
|
|
||||||
import TagOrderStore from '../../stores/TagOrderStore';
|
import TagOrderStore from '../../stores/TagOrderStore';
|
||||||
|
|
||||||
import GroupActions from '../../actions/GroupActions';
|
import GroupActions from '../../actions/GroupActions';
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
|
||||||
|
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
|
import { Droppable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
const TagPanel = React.createClass({
|
const TagPanel = React.createClass({
|
||||||
displayName: 'TagPanel',
|
displayName: 'TagPanel',
|
||||||
|
@ -36,17 +36,7 @@ const TagPanel = React.createClass({
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
// A list of group profiles for tags that are group IDs. The intention in future
|
orderedTags: [],
|
||||||
// is to allow arbitrary tags to be selected in the TagPanel, not just groups.
|
|
||||||
// For now, it suffices to maintain a list of ordered group profiles.
|
|
||||||
orderedGroupTagProfiles: [
|
|
||||||
// {
|
|
||||||
// groupId: '+awesome:foo.bar',{
|
|
||||||
// name: 'My Awesome Community',
|
|
||||||
// avatarUrl: 'mxc://...',
|
|
||||||
// shortDescription: 'Some description...',
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -54,28 +44,15 @@ const TagPanel = React.createClass({
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
this.context.matrixClient.on("sync", this._onClientSync);
|
||||||
|
|
||||||
this._filterStoreToken = FilterStore.addListener(() => {
|
|
||||||
if (this.unmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
selectedTags: FilterStore.getSelectedTags(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.setState({
|
||||||
const orderedTags = TagOrderStore.getOrderedTags() || [];
|
orderedTags: TagOrderStore.getOrderedTags() || [],
|
||||||
const orderedGroupTags = orderedTags.filter((t) => t[0] === '+');
|
selectedTags: TagOrderStore.getSelectedTags(),
|
||||||
// XXX: One profile lookup failing will bring the whole lot down
|
|
||||||
Promise.all(orderedGroupTags.map(
|
|
||||||
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
|
||||||
)).then((orderedGroupTagProfiles) => {
|
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({orderedGroupTagProfiles});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// This could be done by anything with a matrix client
|
// This could be done by anything with a matrix client
|
||||||
|
@ -85,6 +62,7 @@ const TagPanel = React.createClass({
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
this.context.matrixClient.removeListener("sync", this._onClientSync);
|
||||||
if (this._filterStoreToken) {
|
if (this._filterStoreToken) {
|
||||||
this._filterStoreToken.remove();
|
this._filterStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -95,7 +73,17 @@ const TagPanel = React.createClass({
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick() {
|
_onClientSync(syncState, prevState) {
|
||||||
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
|
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||||
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
|
if (reconnected) {
|
||||||
|
// Load joined groups
|
||||||
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseDown(e) {
|
||||||
dis.dispatch({action: 'deselect_tags'});
|
dis.dispatch({action: 'deselect_tags'});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -104,30 +92,65 @@ const TagPanel = React.createClass({
|
||||||
dis.dispatch({action: 'view_create_group'});
|
dis.dispatch({action: 'view_create_group'});
|
||||||
},
|
},
|
||||||
|
|
||||||
onTagTileEndDrag() {
|
onClearFilterClick(ev) {
|
||||||
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
|
dis.dispatch({action: 'deselect_tags'});
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||||
|
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
|
|
||||||
const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => {
|
|
||||||
|
const tags = this.state.orderedTags.map((tag, index) => {
|
||||||
return <DNDTagTile
|
return <DNDTagTile
|
||||||
key={groupProfile.groupId + '_' + index}
|
key={tag}
|
||||||
groupProfile={groupProfile}
|
tag={tag}
|
||||||
selected={this.state.selectedTags.includes(groupProfile.groupId)}
|
index={index}
|
||||||
onEndDrag={this.onTagTileEndDrag}
|
selected={this.state.selectedTags.includes(tag)}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
return <div className="mx_TagPanel" onClick={this.onClick}>
|
|
||||||
<div className="mx_TagPanel_tagTileContainer">
|
const clearButton = this.state.selectedTags.length > 0 ?
|
||||||
{ tags }
|
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||||
</div>
|
alt={_t("Clear filter")}
|
||||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
title={_t("Clear filter")}
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
/> :
|
||||||
|
<div />;
|
||||||
|
|
||||||
|
return <div className="mx_TagPanel">
|
||||||
|
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||||
|
{ clearButton }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
<div className="mx_TagPanel_divider" />
|
||||||
|
<GeminiScrollbarWrapper
|
||||||
|
className="mx_TagPanel_scroller"
|
||||||
|
autoshow={true}
|
||||||
|
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||||
|
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
|
||||||
|
onMouseDown={this.onMouseDown}
|
||||||
|
>
|
||||||
|
<Droppable
|
||||||
|
droppableId="tag-panel-droppable"
|
||||||
|
type="draggable-TagTile"
|
||||||
|
>
|
||||||
|
{ (provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className="mx_TagPanel_tagTileContainer"
|
||||||
|
ref={provided.innerRef}
|
||||||
|
>
|
||||||
|
{ tags }
|
||||||
|
{ provided.placeholder }
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</Droppable>
|
||||||
|
</GeminiScrollbarWrapper>
|
||||||
|
<div className="mx_TagPanel_divider" />
|
||||||
|
<div className="mx_TagPanel_groupsButton">
|
||||||
|
<GroupsButton tooltip={true} />
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
const Matrix = require("matrix-js-sdk");
|
const Matrix = require("matrix-js-sdk");
|
||||||
|
@ -58,49 +59,49 @@ var TimelinePanel = React.createClass({
|
||||||
// representing. This may or may not have a room, depending on what it's
|
// representing. This may or may not have a room, depending on what it's
|
||||||
// a timeline representing. If it has a room, we maintain RRs etc for
|
// a timeline representing. If it has a room, we maintain RRs etc for
|
||||||
// that room.
|
// that room.
|
||||||
timelineSet: React.PropTypes.object.isRequired,
|
timelineSet: PropTypes.object.isRequired,
|
||||||
|
|
||||||
showReadReceipts: React.PropTypes.bool,
|
showReadReceipts: PropTypes.bool,
|
||||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||||
manageReadReceipts: React.PropTypes.bool,
|
manageReadReceipts: PropTypes.bool,
|
||||||
manageReadMarkers: React.PropTypes.bool,
|
manageReadMarkers: PropTypes.bool,
|
||||||
|
|
||||||
// true to give the component a 'display: none' style.
|
// true to give the component a 'display: none' style.
|
||||||
hidden: React.PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
|
||||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||||
// typically this will be either 'eventId' or undefined.
|
// typically this will be either 'eventId' or undefined.
|
||||||
highlightedEventId: React.PropTypes.string,
|
highlightedEventId: PropTypes.string,
|
||||||
|
|
||||||
// id of an event to jump to. If not given, will go to the end of the
|
// id of an event to jump to. If not given, will go to the end of the
|
||||||
// live timeline.
|
// live timeline.
|
||||||
eventId: React.PropTypes.string,
|
eventId: PropTypes.string,
|
||||||
|
|
||||||
// where to position the event given by eventId, in pixels from the
|
// where to position the event given by eventId, in pixels from the
|
||||||
// bottom of the viewport. If not given, will try to put the event
|
// bottom of the viewport. If not given, will try to put the event
|
||||||
// half way down the viewport.
|
// half way down the viewport.
|
||||||
eventPixelOffset: React.PropTypes.number,
|
eventPixelOffset: PropTypes.number,
|
||||||
|
|
||||||
// Should we show URL Previews
|
// Should we show URL Previews
|
||||||
showUrlPreview: React.PropTypes.bool,
|
showUrlPreview: PropTypes.bool,
|
||||||
|
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when the read-up-to mark is updated.
|
// callback which is called when the read-up-to mark is updated.
|
||||||
onReadMarkerUpdated: React.PropTypes.func,
|
onReadMarkerUpdated: PropTypes.func,
|
||||||
|
|
||||||
// maximum number of events to show in a timeline
|
// maximum number of events to show in a timeline
|
||||||
timelineCap: React.PropTypes.number,
|
timelineCap: PropTypes.number,
|
||||||
|
|
||||||
// classname to use for the messagepanel
|
// classname to use for the messagepanel
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
|
||||||
// shape property to be passed to EventTiles
|
// shape property to be passed to EventTiles
|
||||||
tileShape: React.PropTypes.string,
|
tileShape: PropTypes.string,
|
||||||
|
|
||||||
// placeholder text to use if the timeline is empty
|
// placeholder text to use if the timeline is empty
|
||||||
empty: React.PropTypes.string,
|
empty: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -301,6 +302,8 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// set off a pagination request.
|
// set off a pagination request.
|
||||||
onMessageListFillRequest: function(backwards) {
|
onMessageListFillRequest: function(backwards) {
|
||||||
|
if (!this._shouldPaginate()) return Promise.resolve(false);
|
||||||
|
|
||||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||||
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||||
|
@ -621,6 +624,7 @@ var TimelinePanel = React.createClass({
|
||||||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'on_room_read',
|
action: 'on_room_read',
|
||||||
|
roomId: this.props.timelineSet.room.roomId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1090,6 +1094,17 @@ var TimelinePanel = React.createClass({
|
||||||
}, this.props.onReadMarkerUpdated);
|
}, this.props.onReadMarkerUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_shouldPaginate: function() {
|
||||||
|
// don't try to paginate while events in the timeline are
|
||||||
|
// still being decrypted. We don't render events while they're
|
||||||
|
// being decrypted, so they don't take up space in the timeline.
|
||||||
|
// This means we can pull quite a lot of events into the timeline
|
||||||
|
// and end up trying to render a lot of events.
|
||||||
|
return !this.state.events.some((e) => {
|
||||||
|
return e.isBeingDecrypted();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -1107,9 +1122,9 @@ var TimelinePanel = React.createClass({
|
||||||
// exist.
|
// exist.
|
||||||
if (this.state.timelineLoading) {
|
if (this.state.timelineLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
<div className="mx_RoomView_messagePanelSpinner">
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
const ContentMessages = require('../../ContentMessages');
|
const ContentMessages = require('../../ContentMessages');
|
||||||
const dis = require('../../dispatcher');
|
const dis = require('../../dispatcher');
|
||||||
const filesize = require('filesize');
|
const filesize = require('filesize');
|
||||||
|
@ -22,7 +23,7 @@ import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({displayName: 'UploadBar',
|
module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object,
|
room: PropTypes.object,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
const sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
const PlatformPeg = require("../../PlatformPeg");
|
const PlatformPeg = require("../../PlatformPeg");
|
||||||
|
@ -29,7 +30,6 @@ import Promise from 'bluebird';
|
||||||
const packageJson = require('../../../package.json');
|
const packageJson = require('../../../package.json');
|
||||||
const UserSettingsStore = require('../../UserSettingsStore');
|
const UserSettingsStore = require('../../UserSettingsStore');
|
||||||
const CallMediaHandler = require('../../CallMediaHandler');
|
const CallMediaHandler = require('../../CallMediaHandler');
|
||||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
|
||||||
const Email = require('../../email');
|
const Email = require('../../email');
|
||||||
const AddThreepid = require('../../AddThreepid');
|
const AddThreepid = require('../../AddThreepid');
|
||||||
const SdkConfig = require('../../SdkConfig');
|
const SdkConfig = require('../../SdkConfig');
|
||||||
|
@ -78,6 +78,7 @@ const SIMPLE_SETTINGS = [
|
||||||
{ id: "Pill.shouldHidePillAvatar" },
|
{ id: "Pill.shouldHidePillAvatar" },
|
||||||
{ id: "TextualBody.disableBigEmoji" },
|
{ id: "TextualBody.disableBigEmoji" },
|
||||||
{ id: "VideoView.flipVideoHorizontally" },
|
{ id: "VideoView.flipVideoHorizontally" },
|
||||||
|
{ id: "TagPanel.disableTagPanel" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// These settings must be defined in SettingsStore
|
// These settings must be defined in SettingsStore
|
||||||
|
@ -125,8 +126,8 @@ const THEMES = [
|
||||||
|
|
||||||
const IgnoredUser = React.createClass({
|
const IgnoredUser = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
onUnignored: React.PropTypes.func.isRequired,
|
onUnignored: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
_onUnignoreClick: function() {
|
_onUnignoreClick: function() {
|
||||||
|
@ -155,16 +156,16 @@ module.exports = React.createClass({
|
||||||
displayName: 'UserSettings',
|
displayName: 'UserSettings',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onClose: React.PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
// The brand string given when creating email pushers
|
// The brand string given when creating email pushers
|
||||||
brand: React.PropTypes.string,
|
brand: PropTypes.string,
|
||||||
|
|
||||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||||
referralBaseUrl: React.PropTypes.string,
|
referralBaseUrl: PropTypes.string,
|
||||||
|
|
||||||
// Team token for the referral link. If falsy, the referral section will
|
// Team token for the referral link. If falsy, the referral section will
|
||||||
// not appear
|
// not appear
|
||||||
teamToken: React.PropTypes.string,
|
teamToken: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -375,7 +376,7 @@ module.exports = React.createClass({
|
||||||
{ _t("For security, logging out will delete any end-to-end " +
|
{ _t("For security, logging out will delete any end-to-end " +
|
||||||
"encryption keys from this browser. If you want to be able " +
|
"encryption keys from this browser. If you want to be able " +
|
||||||
"to decrypt your conversation history from future Riot sessions, " +
|
"to decrypt your conversation history from future Riot sessions, " +
|
||||||
"please export your room keys for safe-keeping.") }.
|
"please export your room keys for safe-keeping.") }
|
||||||
</div>,
|
</div>,
|
||||||
button: _t("Sign out"),
|
button: _t("Sign out"),
|
||||||
extraButtons: [
|
extraButtons: [
|
||||||
|
@ -793,11 +794,18 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("Bug Report") }</h3>
|
<h3>{ _t("Debug Logs Submission") }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<p>{ _t("Found a bug?") }</p>
|
<p>{
|
||||||
|
_t( "If you've submitted a bug via GitHub, debug logs can help " +
|
||||||
|
"us track down the problem. Debug logs contain application " +
|
||||||
|
"usage data including your username, the IDs or aliases of " +
|
||||||
|
"the rooms or groups you have visited and the usernames of " +
|
||||||
|
"other users. They do not contian messages.",
|
||||||
|
)
|
||||||
|
}</p>
|
||||||
<button className="mx_UserSettings_button danger"
|
<button className="mx_UserSettings_button danger"
|
||||||
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -811,6 +819,12 @@ module.exports = React.createClass({
|
||||||
<h3>{ _t('Analytics') }</h3>
|
<h3>{ _t('Analytics') }</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
||||||
|
<br />
|
||||||
|
{ _t('Privacy is important to us, so we don\'t collect any personal'
|
||||||
|
+ ' or identifiable data for our analytics.') }
|
||||||
|
<div className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
|
||||||
|
{ _t('Learn more about how we use analytics.') }
|
||||||
|
</div>
|
||||||
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
|
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -1103,6 +1117,7 @@ module.exports = React.createClass({
|
||||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
const Notifications = sdk.getComponent("settings.Notifications");
|
const Notifications = sdk.getComponent("settings.Notifications");
|
||||||
const EditableText = sdk.getComponent('elements.EditableText');
|
const EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
|
|
||||||
const avatarUrl = (
|
const avatarUrl = (
|
||||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||||
|
@ -1198,8 +1213,9 @@ module.exports = React.createClass({
|
||||||
onCancelClick={this.props.onClose}
|
onCancelClick={this.props.onClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GeminiScrollbar className="mx_UserSettings_body"
|
<GeminiScrollbarWrapper
|
||||||
autoshow={true}>
|
className="mx_UserSettings_body"
|
||||||
|
autoshow={true}>
|
||||||
|
|
||||||
<h3>{ _t("Profile") }</h3>
|
<h3>{ _t("Profile") }</h3>
|
||||||
|
|
||||||
|
@ -1312,7 +1328,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
{ this._renderDeactivateAccount() }
|
{ this._renderDeactivateAccount() }
|
||||||
|
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbarWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,10 +18,12 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
|
||||||
import PasswordReset from "../../../PasswordReset";
|
import PasswordReset from "../../../PasswordReset";
|
||||||
|
|
||||||
|
@ -29,13 +31,13 @@ module.exports = React.createClass({
|
||||||
displayName: 'ForgotPassword',
|
displayName: 'ForgotPassword',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
defaultHsUrl: React.PropTypes.string,
|
defaultHsUrl: PropTypes.string,
|
||||||
defaultIsUrl: React.PropTypes.string,
|
defaultIsUrl: PropTypes.string,
|
||||||
customHsUrl: React.PropTypes.string,
|
customHsUrl: PropTypes.string,
|
||||||
customIsUrl: React.PropTypes.string,
|
customIsUrl: PropTypes.string,
|
||||||
onLoginClick: React.PropTypes.func,
|
onLoginClick: PropTypes.func,
|
||||||
onRegisterClick: React.PropTypes.func,
|
onRegisterClick: PropTypes.func,
|
||||||
onComplete: React.PropTypes.func.isRequired,
|
onComplete: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -184,7 +186,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let serverConfigSection;
|
let serverConfigSection;
|
||||||
if (!config.disable_custom_urls) {
|
if (!SdkConfig.get().disable_custom_urls) {
|
||||||
serverConfigSection = (
|
serverConfigSection = (
|
||||||
<ServerConfig ref="serverConfig"
|
<ServerConfig ref="serverConfig"
|
||||||
withToggleButton={true}
|
withToggleButton={true}
|
||||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as languageHandler from '../../../languageHandler';
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
@ -36,27 +37,28 @@ module.exports = React.createClass({
|
||||||
displayName: 'Login',
|
displayName: 'Login',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onLoggedIn: React.PropTypes.func.isRequired,
|
onLoggedIn: PropTypes.func.isRequired,
|
||||||
|
|
||||||
enableGuest: React.PropTypes.bool,
|
enableGuest: PropTypes.bool,
|
||||||
|
|
||||||
customHsUrl: React.PropTypes.string,
|
customHsUrl: PropTypes.string,
|
||||||
customIsUrl: React.PropTypes.string,
|
customIsUrl: PropTypes.string,
|
||||||
defaultHsUrl: React.PropTypes.string,
|
defaultHsUrl: PropTypes.string,
|
||||||
defaultIsUrl: React.PropTypes.string,
|
defaultIsUrl: PropTypes.string,
|
||||||
// Secondary HS which we try to log into if the user is using
|
// Secondary HS which we try to log into if the user is using
|
||||||
// the default HS but login fails. Useful for migrating to a
|
// the default HS but login fails. Useful for migrating to a
|
||||||
// different home server without confusing users.
|
// different home server without confusing users.
|
||||||
fallbackHsUrl: React.PropTypes.string,
|
fallbackHsUrl: PropTypes.string,
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: PropTypes.string,
|
||||||
|
|
||||||
// login shouldn't know or care how registration is done.
|
// login shouldn't know or care how registration is done.
|
||||||
onRegisterClick: React.PropTypes.func.isRequired,
|
onRegisterClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// login shouldn't care how password recovery is done.
|
// login shouldn't care how password recovery is done.
|
||||||
onForgotPasswordClick: React.PropTypes.func,
|
onForgotPasswordClick: PropTypes.func,
|
||||||
onCancelClick: React.PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
|
onServerConfigChange: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -217,6 +219,8 @@ module.exports = React.createClass({
|
||||||
if (config.isUrl !== undefined) {
|
if (config.isUrl !== undefined) {
|
||||||
newState.enteredIdentityServerUrl = config.isUrl;
|
newState.enteredIdentityServerUrl = config.isUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props.onServerConfigChange(config);
|
||||||
this.setState(newState, function() {
|
this.setState(newState, function() {
|
||||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||||
});
|
});
|
||||||
|
@ -427,10 +431,10 @@ module.exports = React.createClass({
|
||||||
// FIXME: remove status.im theme tweaks
|
// FIXME: remove status.im theme tweaks
|
||||||
const theme = SettingsStore.getValue("theme");
|
const theme = SettingsStore.getValue("theme");
|
||||||
if (theme !== "status") {
|
if (theme !== "status") {
|
||||||
header = <h2>{ _t('Sign in') }</h2>;
|
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||||
} else {
|
} else {
|
||||||
if (!this.state.errorText) {
|
if (!this.state.errorText) {
|
||||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -25,7 +26,7 @@ module.exports = React.createClass({
|
||||||
displayName: 'PostRegistration',
|
displayName: 'PostRegistration',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onComplete: React.PropTypes.func.isRequired,
|
onComplete: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import ServerConfig from '../../views/login/ServerConfig';
|
import ServerConfig from '../../views/login/ServerConfig';
|
||||||
|
@ -35,31 +36,32 @@ module.exports = React.createClass({
|
||||||
displayName: 'Registration',
|
displayName: 'Registration',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onLoggedIn: React.PropTypes.func.isRequired,
|
onLoggedIn: PropTypes.func.isRequired,
|
||||||
clientSecret: React.PropTypes.string,
|
clientSecret: PropTypes.string,
|
||||||
sessionId: React.PropTypes.string,
|
sessionId: PropTypes.string,
|
||||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||||
idSid: React.PropTypes.string,
|
idSid: PropTypes.string,
|
||||||
customHsUrl: React.PropTypes.string,
|
customHsUrl: PropTypes.string,
|
||||||
customIsUrl: React.PropTypes.string,
|
customIsUrl: PropTypes.string,
|
||||||
defaultHsUrl: React.PropTypes.string,
|
defaultHsUrl: PropTypes.string,
|
||||||
defaultIsUrl: React.PropTypes.string,
|
defaultIsUrl: PropTypes.string,
|
||||||
brand: React.PropTypes.string,
|
brand: PropTypes.string,
|
||||||
email: React.PropTypes.string,
|
email: PropTypes.string,
|
||||||
referrer: React.PropTypes.string,
|
referrer: PropTypes.string,
|
||||||
teamServerConfig: React.PropTypes.shape({
|
teamServerConfig: PropTypes.shape({
|
||||||
// Email address to request new teams
|
// Email address to request new teams
|
||||||
supportEmail: React.PropTypes.string.isRequired,
|
supportEmail: PropTypes.string.isRequired,
|
||||||
// URL of the riot-team-server to get team configurations and track referrals
|
// URL of the riot-team-server to get team configurations and track referrals
|
||||||
teamServerURL: React.PropTypes.string.isRequired,
|
teamServerURL: PropTypes.string.isRequired,
|
||||||
}),
|
}),
|
||||||
teamSelected: React.PropTypes.object,
|
teamSelected: PropTypes.object,
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: PropTypes.string,
|
||||||
|
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
onLoginClick: React.PropTypes.func.isRequired,
|
onLoginClick: PropTypes.func.isRequired,
|
||||||
onCancelClick: React.PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
|
onServerConfigChange: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -130,6 +132,7 @@ module.exports = React.createClass({
|
||||||
if (config.isUrl !== undefined) {
|
if (config.isUrl !== undefined) {
|
||||||
newState.isUrl = config.isUrl;
|
newState.isUrl = config.isUrl;
|
||||||
}
|
}
|
||||||
|
this.props.onServerConfigChange(config);
|
||||||
this.setState(newState, function() {
|
this.setState(newState, function() {
|
||||||
this._replaceClient();
|
this._replaceClient();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,6 +16,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import AvatarLogic from '../../../Avatar';
|
import AvatarLogic from '../../../Avatar';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -23,16 +26,20 @@ module.exports = React.createClass({
|
||||||
displayName: 'BaseAvatar',
|
displayName: 'BaseAvatar',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
|
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||||
idName: React.PropTypes.string, // ID for generating hash colours
|
idName: PropTypes.string, // ID for generating hash colours
|
||||||
title: React.PropTypes.string, // onHover title text
|
title: PropTypes.string, // onHover title text
|
||||||
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||||
width: React.PropTypes.number,
|
width: PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: PropTypes.number,
|
||||||
// XXX resizeMethod not actually used.
|
// XXX resizeMethod not actually used.
|
||||||
resizeMethod: React.PropTypes.string,
|
resizeMethod: PropTypes.string,
|
||||||
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
|
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||||
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -48,6 +55,16 @@ module.exports = React.createClass({
|
||||||
return this._getState(this.props);
|
return this._getState(this.props);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.unmounted = false;
|
||||||
|
this.context.matrixClient.on('sync', this.onClientSync);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||||
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
// work out if we need to call setState (if the image URLs array has changed)
|
// work out if we need to call setState (if the image URLs array has changed)
|
||||||
const newState = this._getState(nextProps);
|
const newState = this._getState(nextProps);
|
||||||
|
@ -66,6 +83,23 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClientSync: function(syncState, prevState) {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
|
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||||
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
|
if (reconnected &&
|
||||||
|
// Did we fall back?
|
||||||
|
this.state.urlsIndex > 0
|
||||||
|
) {
|
||||||
|
// Start from the highest priority URL again
|
||||||
|
this.setState({
|
||||||
|
urlsIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_getState: function(props) {
|
_getState: function(props) {
|
||||||
// work out the full set of urls to try to load. This is formed like so:
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
// imageUrls: [ props.url, props.urls, default image ]
|
// imageUrls: [ props.url, props.urls, default image ]
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
const Avatar = require('../../../Avatar');
|
const Avatar = require('../../../Avatar');
|
||||||
const sdk = require("../../../index");
|
const sdk = require("../../../index");
|
||||||
const dispatcher = require("../../../dispatcher");
|
const dispatcher = require("../../../dispatcher");
|
||||||
|
@ -25,15 +26,15 @@ module.exports = React.createClass({
|
||||||
displayName: 'MemberAvatar',
|
displayName: 'MemberAvatar',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
member: React.PropTypes.object.isRequired,
|
member: PropTypes.object.isRequired,
|
||||||
width: React.PropTypes.number,
|
width: PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: PropTypes.number,
|
||||||
resizeMethod: React.PropTypes.string,
|
resizeMethod: PropTypes.string,
|
||||||
// The onClick to give the avatar
|
// The onClick to give the avatar
|
||||||
onClick: React.PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||||
viewUserOnClick: React.PropTypes.bool,
|
viewUserOnClick: PropTypes.bool,
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {ContentRepo} from "matrix-js-sdk";
|
import {ContentRepo} from "matrix-js-sdk";
|
||||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import sdk from "../../../index";
|
import sdk from "../../../index";
|
||||||
|
@ -25,11 +26,11 @@ module.exports = React.createClass({
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object,
|
room: PropTypes.object,
|
||||||
oobData: React.PropTypes.object,
|
oobData: PropTypes.object,
|
||||||
width: React.PropTypes.number,
|
width: PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: PropTypes.number,
|
||||||
resizeMethod: React.PropTypes.string,
|
resizeMethod: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -47,12 +48,34 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (cli) {
|
||||||
|
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
urls: this.getImageUrls(newProps),
|
urls: this.getImageUrls(newProps),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomStateEvents: function(ev) {
|
||||||
|
if (!this.props.room ||
|
||||||
|
ev.getRoomId() !== this.props.room.roomId ||
|
||||||
|
ev.getType() !== 'm.room.avatar'
|
||||||
|
) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
urls: this.getImageUrls(this.props),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getImageUrls: function(props) {
|
getImageUrls: function(props) {
|
||||||
return [
|
return [
|
||||||
ContentRepo.getHttpUriForMxc(
|
ContentRepo.getHttpUriForMxc(
|
||||||
|
@ -86,10 +109,15 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const mlist = props.room.currentState.members;
|
const mlist = props.room.currentState.members;
|
||||||
const userIds = [];
|
const userIds = [];
|
||||||
|
const leftUserIds = [];
|
||||||
// for .. in optimisation to return early if there are >2 keys
|
// for .. in optimisation to return early if there are >2 keys
|
||||||
for (const uid in mlist) {
|
for (const uid in mlist) {
|
||||||
if (mlist.hasOwnProperty(uid)) {
|
if (mlist.hasOwnProperty(uid)) {
|
||||||
userIds.push(uid);
|
if (["join", "invite"].includes(mlist[uid].membership)) {
|
||||||
|
userIds.push(uid);
|
||||||
|
} else {
|
||||||
|
leftUserIds.push(uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (userIds.length > 2) {
|
if (userIds.length > 2) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -111,6 +139,14 @@ module.exports = React.createClass({
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
} else if (userIds.length == 1) {
|
} else if (userIds.length == 1) {
|
||||||
|
// The other 1-1 user left, leaving just the current user, so show the left user's avatar
|
||||||
|
if (leftUserIds.length === 1) {
|
||||||
|
return mlist[leftUserIds[0]].getAvatarUrl(
|
||||||
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
props.width, props.height, props.resizeMethod,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
return mlist[userIds[0]].getAvatarUrl(
|
return mlist[userIds[0]].getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
|
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'CreateRoomButton',
|
displayName: 'CreateRoomButton',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onCreateRoom: React.PropTypes.func,
|
onCreateRoom: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
const Presets = {
|
const Presets = {
|
||||||
|
@ -28,8 +29,8 @@ const Presets = {
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'CreateRoomPresets',
|
displayName: 'CreateRoomPresets',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
preset: React.PropTypes.string,
|
preset: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
Presets: Presets,
|
Presets: Presets,
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -22,9 +23,9 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// Specifying a homeserver will make magical things happen when you,
|
// Specifying a homeserver will make magical things happen when you,
|
||||||
// e.g. start typing in the room alias box.
|
// e.g. start typing in the room alias box.
|
||||||
homeserver: React.PropTypes.string,
|
homeserver: PropTypes.string,
|
||||||
alias: React.PropTypes.string,
|
alias: PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
|
|
@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
|
@ -507,7 +506,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||||
this.scrollElement = null;
|
this.scrollElement = null;
|
||||||
|
|
||||||
|
@ -580,14 +580,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
|
||||||
<div className="mx_Dialog_title">
|
onFinished={this.props.onFinished} title={this.props.title}>
|
||||||
{ this.props.title }
|
|
||||||
</div>
|
|
||||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
|
||||||
onClick={this.onCancel} >
|
|
||||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
|
||||||
</AccessibleButton>
|
|
||||||
<div className="mx_ChatInviteDialog_label">
|
<div className="mx_ChatInviteDialog_label">
|
||||||
<label htmlFor="textinput">{ this.props.description }</label>
|
<label htmlFor="textinput">{ this.props.description }</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -597,12 +591,10 @@ module.exports = React.createClass({
|
||||||
{ addressSelector }
|
{ addressSelector }
|
||||||
{ this.props.extraNode }
|
{ this.props.extraNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={this.props.button}
|
||||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
onPrimaryButtonClick={this.onButtonClick}
|
||||||
{ this.props.button }
|
onCancel={this.onCancel} />
|
||||||
</button>
|
</BaseDialog>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,10 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import { KeyCode } from '../../../Keyboard';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic container for modal dialogs.
|
* Basic container for modal dialogs.
|
||||||
|
@ -31,33 +36,48 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// onFinished callback to call when Escape is pressed
|
// onFinished callback to call when Escape is pressed
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// callback to call when Enter is pressed
|
// called when a key is pressed
|
||||||
onEnterPressed: React.PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
|
|
||||||
// CSS class to apply to dialog div
|
// CSS class to apply to dialog div
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
|
||||||
// Title for the dialog.
|
// Title for the dialog.
|
||||||
// (could probably actually be something more complicated than a string if desired)
|
// (could probably actually be something more complicated than a string if desired)
|
||||||
title: React.PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// children should be the content of the dialog
|
// children should be the content of the dialog
|
||||||
children: React.PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
|
||||||
|
// Id of content element
|
||||||
|
// If provided, this is used to add a aria-describedby attribute
|
||||||
|
contentId: React.PropTypes.string,
|
||||||
|
},
|
||||||
|
|
||||||
|
childContextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildContext: function() {
|
||||||
|
return {
|
||||||
|
matrixClient: this._matrixClient,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._matrixClient = MatrixClientPeg.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(e) {
|
_onKeyDown: function(e) {
|
||||||
|
if (this.props.onKeyDown) {
|
||||||
|
this.props.onKeyDown(e);
|
||||||
|
}
|
||||||
if (e.keyCode === KeyCode.ESCAPE) {
|
if (e.keyCode === KeyCode.ESCAPE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
} else if (e.keyCode === KeyCode.ENTER) {
|
|
||||||
if (this.props.onEnterPressed) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onEnterPressed(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -69,17 +89,28 @@ export default React.createClass({
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
<FocusTrap onKeyDown={this._onKeyDown}
|
||||||
|
className={this.props.className}
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby='mx_BaseDialog_title'
|
||||||
|
// This should point to a node describing the dialog.
|
||||||
|
// If we were about to completelly follow this recommendation we'd need to
|
||||||
|
// make all the components relying on BaseDialog to be aware of it.
|
||||||
|
// So instead we will use the whole content as the description.
|
||||||
|
// Description comes first and if the content contains more text,
|
||||||
|
// AT users can skip its presentation.
|
||||||
|
aria-describedby={this.props.contentId}
|
||||||
|
>
|
||||||
<AccessibleButton onClick={this._onCancelClick}
|
<AccessibleButton onClick={this._onCancelClick}
|
||||||
className="mx_Dialog_cancelButton"
|
className="mx_Dialog_cancelButton"
|
||||||
>
|
>
|
||||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<div className='mx_Dialog_title'>
|
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>
|
</FocusTrap>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
@ -58,6 +59,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
);
|
);
|
||||||
tiles.push(
|
tiles.push(
|
||||||
<RoomTile key={room.roomId} room={room}
|
<RoomTile key={room.roomId} room={room}
|
||||||
|
transparent={true}
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
selected={false}
|
selected={false}
|
||||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||||
|
@ -127,7 +129,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
|
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
content = <div className="mx_Dialog_content">
|
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
{ _t('You already have existing direct chats with this user:') }
|
{ _t('You already have existing direct chats with this user:') }
|
||||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||||
{ this.state.tiles }
|
{ this.state.tiles }
|
||||||
|
@ -137,6 +139,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
// Show the avatar, name and a button to confirm that a new chat is requested
|
// Show the avatar, name and a button to confirm that a new chat is requested
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
title = _t('Start chatting');
|
title = _t('Start chatting');
|
||||||
|
|
||||||
|
@ -144,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
if (this.state.busyProfile) {
|
if (this.state.busyProfile) {
|
||||||
profile = <Spinner />;
|
profile = <Spinner />;
|
||||||
} else if (this.state.profileError) {
|
} else if (this.state.profileError) {
|
||||||
profile = <div className="error">
|
profile = <div className="error" role="alert">
|
||||||
Unable to load profile information for { this.props.userId }
|
Unable to load profile information for { this.props.userId }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
|
@ -160,17 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
content = <div>
|
content = <div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<p>
|
<p>
|
||||||
{ _t('Click on the button below to start chatting!') }
|
{ _t('Click on the button below to start chatting!') }
|
||||||
</p>
|
</p>
|
||||||
{ profile }
|
{ profile }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||||
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
|
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
|
||||||
{ _t('Start Chatting') }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||||
onFinished={this.props.onFinished.bind(false)}
|
onFinished={this.props.onFinished.bind(false)}
|
||||||
title={title}
|
title={title}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
@ -187,9 +188,9 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatCreateOrReuseDialog.propTyps = {
|
ChatCreateOrReuseDialog.propTyps = {
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
// Called when clicking outside of the dialog
|
// Called when clicking outside of the dialog
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
onNewDMClick: React.PropTypes.func.isRequired,
|
onNewDMClick: PropTypes.func.isRequired,
|
||||||
onExistingRoomSelected: React.PropTypes.func.isRequired,
|
onExistingRoomSelected: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { GroupMemberType } from '../../../groups';
|
import { GroupMemberType } from '../../../groups';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -33,20 +33,20 @@ export default React.createClass({
|
||||||
displayName: 'ConfirmUserActionDialog',
|
displayName: 'ConfirmUserActionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||||
member: React.PropTypes.object,
|
member: PropTypes.object,
|
||||||
// group member object. Supply either this or 'member'
|
// group member object. Supply either this or 'member'
|
||||||
groupMember: GroupMemberType,
|
groupMember: GroupMemberType,
|
||||||
// needed if a group member is specified
|
// needed if a group member is specified
|
||||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
action: PropTypes.string.isRequired, // eg. 'Ban'
|
||||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||||
|
|
||||||
// Whether to display a text field for a reason
|
// Whether to display a text field for a reason
|
||||||
// If true, the second argument to onFinished will
|
// If true, the second argument to onFinished will
|
||||||
// be the string entered.
|
// be the string entered.
|
||||||
askReason: React.PropTypes.bool,
|
askReason: PropTypes.bool,
|
||||||
danger: React.PropTypes.bool,
|
danger: PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
|
@ -76,13 +76,11 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
|
|
||||||
const confirmButtonClass = classnames({
|
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||||
'mx_Dialog_primary': true,
|
|
||||||
'danger': this.props.danger,
|
|
||||||
});
|
|
||||||
|
|
||||||
let reasonBox;
|
let reasonBox;
|
||||||
if (this.props.askReason) {
|
if (this.props.askReason) {
|
||||||
|
@ -116,10 +114,10 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div id="mx_Dialog_content" className="mx_Dialog_content">
|
||||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||||
{ avatar }
|
{ avatar }
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,17 +125,11 @@ export default React.createClass({
|
||||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||||
</div>
|
</div>
|
||||||
{ reasonBox }
|
{ reasonBox }
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={this.props.action}
|
||||||
<button className={confirmButtonClass}
|
onPrimaryButtonClick={this.onOk}
|
||||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
primaryButtonClass={confirmButtonClass}
|
||||||
>
|
focus={!this.props.askReason}
|
||||||
{ this.props.action }
|
onCancel={this.onCancel} />
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onClick={this.onCancel}>
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,11 +55,15 @@ export default React.createClass({
|
||||||
|
|
||||||
_checkGroupId: function(e) {
|
_checkGroupId: function(e) {
|
||||||
let error = null;
|
let error = null;
|
||||||
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
if (!this.state.groupId) {
|
||||||
|
error = _t("Community IDs cannot not be empty.");
|
||||||
|
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
groupIdError: error,
|
groupIdError: error,
|
||||||
|
// Reset createError to get rid of now stale error message
|
||||||
|
createError: null,
|
||||||
});
|
});
|
||||||
return error;
|
return error;
|
||||||
},
|
},
|
||||||
|
@ -108,7 +112,7 @@ export default React.createClass({
|
||||||
// XXX: We should catch errcodes and give sensible i18ned messages for them,
|
// XXX: We should catch errcodes and give sensible i18ned messages for them,
|
||||||
// rather than displaying what the server gives us, but synapse doesn't give
|
// rather than displaying what the server gives us, but synapse doesn't give
|
||||||
// any yet.
|
// any yet.
|
||||||
createErrorNode = <div className="error">
|
createErrorNode = <div className="error" role="alert">
|
||||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||||
<div>{ this.state.createError.message }</div>
|
<div>{ this.state.createError.message }</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -116,7 +120,6 @@ export default React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this._onFormSubmit}
|
|
||||||
title={_t('Create Community')}
|
title={_t('Create Community')}
|
||||||
>
|
>
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
@ -159,10 +162,10 @@ export default React.createClass({
|
||||||
{ createErrorNode }
|
{ createErrorNode }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||||
<button onClick={this._onCancel}>
|
<button onClick={this._onCancel}>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler';
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'CreateRoomDialog',
|
displayName: 'CreateRoomDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -41,40 +42,37 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={_t('Create Room')}
|
title={_t('Create Room')}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<form onSubmit={this.onOk}>
|
||||||
<div className="mx_CreateRoomDialog_label">
|
<div className="mx_Dialog_content">
|
||||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
<div className="mx_CreateRoomDialog_label">
|
||||||
</div>
|
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||||
<div>
|
|
||||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<details className="mx_CreateRoomDialog_details">
|
|
||||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
|
||||||
<div>
|
|
||||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
|
||||||
<label htmlFor="checkbox">
|
|
||||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
|
||||||
<br />
|
|
||||||
({ _t('This setting cannot be changed later!') })
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<div>
|
||||||
</div>
|
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||||
<div className="mx_Dialog_buttons">
|
</div>
|
||||||
<button onClick={this.onCancel}>
|
<br />
|
||||||
{ _t('Cancel') }
|
|
||||||
</button>
|
<details className="mx_CreateRoomDialog_details">
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||||
{ _t('Create Room') }
|
<div>
|
||||||
</button>
|
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||||
</div>
|
<label htmlFor="checkbox">
|
||||||
|
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||||
|
<br />
|
||||||
|
({ _t('This setting cannot be changed later!') })
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<DialogButtons primaryButton={_t('Create Room')}
|
||||||
|
onPrimaryButtonClick={this.onOk}
|
||||||
|
onCancel={this.onCancel} />
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
|
@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
let passwordBoxClass = '';
|
let passwordBoxClass = '';
|
||||||
|
|
||||||
|
@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_DeactivateAccountDialog">
|
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||||
<div className="mx_Dialog_title danger">
|
onFinished={this.props.onFinished}
|
||||||
{ _t("Deactivate Account") }
|
onEnterPressed={this.onOk}
|
||||||
</div>
|
titleClass="danger"
|
||||||
|
title={_t("Deactivate Account")}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||||
|
|
||||||
|
@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
|
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeactivateAccountDialog.propTypes = {
|
DeactivateAccountDialog.propTypes = {
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||||
|
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceVerifyDialog.propTypes = {
|
DeviceVerifyDialog.propTypes = {
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
device: React.PropTypes.object.isRequired,
|
device: PropTypes.object.isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,20 +26,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ErrorDialog',
|
displayName: 'ErrorDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
description: React.PropTypes.oneOfType([
|
description: PropTypes.oneOfType([
|
||||||
React.PropTypes.element,
|
PropTypes.element,
|
||||||
React.PropTypes.string,
|
PropTypes.string,
|
||||||
]),
|
]),
|
||||||
button: React.PropTypes.string,
|
button: PropTypes.string,
|
||||||
focus: React.PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -51,22 +52,18 @@ export default React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
if (this.props.focus) {
|
|
||||||
this.refs.button.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={this.props.title || _t('Error')}>
|
title={this.props.title || _t('Error')}
|
||||||
<div className="mx_Dialog_content">
|
contentId='mx_Dialog_content'
|
||||||
|
>
|
||||||
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
{ this.props.description || _t('An error has occurred.') }
|
{ this.props.description || _t('An error has occurred.') }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||||
{ this.props.button || _t('OK') }
|
{ this.props.button || _t('OK') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -27,22 +28,22 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// matrix client to use for UI auth requests
|
// matrix client to use for UI auth requests
|
||||||
matrixClient: React.PropTypes.object.isRequired,
|
matrixClient: PropTypes.object.isRequired,
|
||||||
|
|
||||||
// response from initial request. If not supplied, will do a request on
|
// response from initial request. If not supplied, will do a request on
|
||||||
// mount.
|
// mount.
|
||||||
authData: React.PropTypes.shape({
|
authData: PropTypes.shape({
|
||||||
flows: React.PropTypes.array,
|
flows: PropTypes.array,
|
||||||
params: React.PropTypes.object,
|
params: PropTypes.object,
|
||||||
session: React.PropTypes.string,
|
session: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// callback
|
// callback
|
||||||
makeRequest: React.PropTypes.func.isRequired,
|
makeRequest: PropTypes.func.isRequired,
|
||||||
|
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -72,11 +73,12 @@ export default React.createClass({
|
||||||
let content;
|
let content;
|
||||||
if (this.state.authError) {
|
if (this.state.authError) {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div id='mx_Dialog_content'>
|
||||||
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
|
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||||
<br />
|
<br />
|
||||||
<AccessibleButton onClick={this._onDismissClick}
|
<AccessibleButton onClick={this._onDismissClick}
|
||||||
className="mx_UserSettings_button"
|
className="mx_UserSettings_button"
|
||||||
|
autoFocus="true"
|
||||||
>
|
>
|
||||||
{ _t("Dismiss") }
|
{ _t("Dismiss") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -84,7 +86,7 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div id='mx_Dialog_content'>
|
||||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||||
matrixClient={this.props.matrixClient}
|
matrixClient={this.props.matrixClient}
|
||||||
authData={this.props.authData}
|
authData={this.props.authData}
|
||||||
|
@ -99,6 +101,7 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler';
|
||||||
*/
|
*/
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: React.PropTypes.object.isRequired,
|
matrixClient: PropTypes.object.isRequired,
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
deviceId: React.PropTypes.string.isRequired,
|
deviceId: PropTypes.string.isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -125,11 +126,11 @@ export default React.createClass({
|
||||||
text = _t(text, {displayName: displayName});
|
text = _t(text, {displayName: displayName});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div id='mx_Dialog_content'>
|
||||||
<p>{ text }</p>
|
<p>{ text }</p>
|
||||||
|
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this._onVerifyClicked}>
|
<button onClick={this._onVerifyClicked} autoFocus="true">
|
||||||
{ _t('Start verification') }
|
{ _t('Start verification') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onShareClicked}>
|
<button onClick={this._onShareClicked}>
|
||||||
|
@ -153,7 +154,7 @@ export default React.createClass({
|
||||||
content = this._renderContent();
|
content = this._renderContent();
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div id='mx_Dialog_content'>
|
||||||
<p>{ _t('Loading device info...') }</p>
|
<p>{ _t('Loading device info...') }</p>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
|
@ -164,6 +165,7 @@ export default React.createClass({
|
||||||
<BaseDialog className='mx_KeyShareRequestDialog'
|
<BaseDialog className='mx_KeyShareRequestDialog'
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Encryption key request')}
|
title={_t('Encryption key request')}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -16,20 +16,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'QuestionDialog',
|
displayName: 'QuestionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
description: React.PropTypes.node,
|
description: PropTypes.node,
|
||||||
extraButtons: React.PropTypes.node,
|
extraButtons: PropTypes.node,
|
||||||
button: React.PropTypes.string,
|
button: PropTypes.string,
|
||||||
danger: React.PropTypes.bool,
|
danger: PropTypes.bool,
|
||||||
focus: React.PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -53,30 +53,27 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const cancelButton = this.props.hasCancelButton ? (
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
<button onClick={this.onCancel}>
|
let primaryButtonClass = "";
|
||||||
{ _t("Cancel") }
|
if (this.props.danger) {
|
||||||
</button>
|
primaryButtonClass = "danger";
|
||||||
) : null;
|
}
|
||||||
const buttonClasses = classnames({
|
|
||||||
mx_Dialog_primary: true,
|
|
||||||
danger: this.props.danger,
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
{ this.props.description }
|
{ this.props.description }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||||
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
onPrimaryButtonClick={this.onOk}
|
||||||
{ this.props.button || _t('OK') }
|
primaryButtonClass={primaryButtonClass}
|
||||||
</button>
|
focus={this.props.focus}
|
||||||
|
onCancel={this.onCancel}
|
||||||
|
>
|
||||||
{ this.props.extraButtons }
|
{ this.props.extraButtons }
|
||||||
{ cancelButton }
|
</DialogButtons>
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -25,8 +26,14 @@ export default React.createClass({
|
||||||
displayName: 'SessionRestoreErrorDialog',
|
displayName: 'SessionRestoreErrorDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
error: React.PropTypes.string.isRequired,
|
error: PropTypes.string.isRequired,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
if (this.refs.bugreportLink) {
|
||||||
|
this.refs.bugreportLink.focus();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_sendBugReport: function() {
|
_sendBugReport: function() {
|
||||||
|
@ -40,6 +47,7 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
let bugreport;
|
let bugreport;
|
||||||
|
|
||||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||||
|
@ -48,16 +56,20 @@ export default React.createClass({
|
||||||
{ _t(
|
{ _t(
|
||||||
"Otherwise, <a>click here</a> to send a bug report.",
|
"Otherwise, <a>click here</a> to send a bug report.",
|
||||||
{},
|
{},
|
||||||
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
|
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
|
||||||
|
key="bugreport" href='#'>{ sub }</a> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const shouldFocusContinueButton =!(bugreport==true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
title={_t('Unable to restore session')}>
|
title={_t('Unable to restore session')}
|
||||||
<div className="mx_Dialog_content">
|
contentId='mx_Dialog_content'
|
||||||
|
>
|
||||||
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
||||||
"you continue, you will need to log in again, and encrypted chat " +
|
"you continue, you will need to log in again, and encrypted chat " +
|
||||||
"history will be unreadable.") }</p>
|
"history will be unreadable.") }</p>
|
||||||
|
@ -68,11 +80,9 @@ export default React.createClass({
|
||||||
|
|
||||||
{ bugreport }
|
{ bugreport }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={_t("Continue anyway")}
|
||||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
|
||||||
{ _t("Continue anyway") }
|
onCancel={this.props.onFinished} />
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Email from '../../../email';
|
import Email from '../../../email';
|
||||||
import AddThreepid from '../../../AddThreepid';
|
import AddThreepid from '../../../AddThreepid';
|
||||||
|
@ -30,7 +31,7 @@ import Modal from '../../../Modal';
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'SetEmailDialog',
|
displayName: 'SetEmailDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -40,9 +41,6 @@ export default React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
},
|
|
||||||
|
|
||||||
onEmailAddressChanged: function(value) {
|
onEmailAddressChanged: function(value) {
|
||||||
this.setState({
|
this.setState({
|
||||||
emailAddress: value,
|
emailAddress: value,
|
||||||
|
@ -130,6 +128,7 @@ export default React.createClass({
|
||||||
|
|
||||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||||
className="mx_SetEmailDialog_email_input"
|
className="mx_SetEmailDialog_email_input"
|
||||||
|
autoFocus="true"
|
||||||
placeholder={_t("Email address")}
|
placeholder={_t("Email address")}
|
||||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||||
blurToCancel={false}
|
blurToCancel={false}
|
||||||
|
@ -139,9 +138,10 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_SetEmailDialog"
|
<BaseDialog className="mx_SetEmailDialog"
|
||||||
onFinished={this.onCancelled}
|
onFinished={this.onCancelled}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>
|
<p id='mx_Dialog_content'>
|
||||||
{ _t('This will allow you to reset your password and receive notifications.') }
|
{ _t('This will allow you to reset your password and receive notifications.') }
|
||||||
</p>
|
</p>
|
||||||
{ emailInput }
|
{ emailInput }
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'SetMxIdDialog',
|
displayName: 'SetMxIdDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
// Called when the user requests to register with a different homeserver
|
// Called when the user requests to register with a different homeserver
|
||||||
onDifferentServerClicked: React.PropTypes.func.isRequired,
|
onDifferentServerClicked: PropTypes.func.isRequired,
|
||||||
// Called if the user wants to switch to login instead
|
// Called if the user wants to switch to login instead
|
||||||
onLoginClick: React.PropTypes.func.isRequired,
|
onLoginClick: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -234,14 +235,14 @@ export default React.createClass({
|
||||||
"error": Boolean(this.state.usernameError),
|
"error": Boolean(this.state.usernameError),
|
||||||
"success": usernameAvailable,
|
"success": usernameAvailable,
|
||||||
});
|
});
|
||||||
usernameIndicator = <div className={usernameIndicatorClasses}>
|
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
|
||||||
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let authErrorIndicator = null;
|
let authErrorIndicator = null;
|
||||||
if (this.state.authError) {
|
if (this.state.authError) {
|
||||||
authErrorIndicator = <div className="error">
|
authErrorIndicator = <div className="error" role="alert">
|
||||||
{ this.state.authError }
|
{ this.state.authError }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -253,8 +254,9 @@ export default React.createClass({
|
||||||
<BaseDialog className="mx_SetMxIdDialog"
|
<BaseDialog className="mx_SetMxIdDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t('To get started, please pick a username!')}
|
title={_t('To get started, please pick a username!')}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<div className="mx_SetMxIdDialog_input_group">
|
<div className="mx_SetMxIdDialog_input_group">
|
||||||
<input type="text" ref="input_value" value={this.state.username}
|
<input type="text" ref="input_value" value={this.state.username}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
|
|
@ -15,21 +15,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'TextInputDialog',
|
displayName: 'TextInputDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
description: React.PropTypes.oneOfType([
|
description: PropTypes.oneOfType([
|
||||||
React.PropTypes.element,
|
PropTypes.element,
|
||||||
React.PropTypes.string,
|
PropTypes.string,
|
||||||
]),
|
]),
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
button: React.PropTypes.string,
|
button: PropTypes.string,
|
||||||
focus: React.PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -58,27 +58,24 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||||
onEnterPressed={this.onOk}
|
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<form onSubmit={this.onOk}>
|
||||||
<div className="mx_TextInputDialog_label">
|
<div className="mx_Dialog_content">
|
||||||
<label htmlFor="textinput"> { this.props.description } </label>
|
<div className="mx_TextInputDialog_label">
|
||||||
|
<label htmlFor="textinput"> { this.props.description } </label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</form>
|
||||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
<DialogButtons primaryButton={this.props.button}
|
||||||
</div>
|
onPrimaryButtonClick={this.onOk}
|
||||||
</div>
|
onCancel={this.onCancel} />
|
||||||
<div className="mx_Dialog_buttons">
|
|
||||||
<button onClick={this.onCancel}>
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</button>
|
|
||||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
|
||||||
{ this.props.button }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
import Resend from '../../../Resend';
|
import Resend from '../../../Resend';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { markAllDevicesKnown } from '../../../cryptodevices';
|
||||||
function markAllDevicesKnown(devices) {
|
|
||||||
Object.keys(devices).forEach((userId) => {
|
|
||||||
Object.keys(devices[userId]).map((deviceId) => {
|
|
||||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeviceListEntry(props) {
|
function DeviceListEntry(props) {
|
||||||
const {userId, device} = props;
|
const {userId, device} = props;
|
||||||
|
@ -141,7 +134,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onSendAnywayClicked: function() {
|
_onSendAnywayClicked: function() {
|
||||||
markAllDevicesKnown(this.props.devices);
|
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
|
||||||
|
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
this.props.onSend();
|
this.props.onSend();
|
||||||
|
@ -153,6 +146,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
if (this.props.devices === null) {
|
if (this.props.devices === null) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
|
@ -187,24 +181,18 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let sendButton;
|
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
|
||||||
if (haveUnknownDevices) {
|
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
|
||||||
sendButton = <button onClick={this._onSendAnywayClicked}>
|
|
||||||
{ this.props.sendAnywayLabel }
|
|
||||||
</button>;
|
|
||||||
} else {
|
|
||||||
sendButton = <button onClick={this._onSendClicked}>
|
|
||||||
{ this.props.sendLabel }
|
|
||||||
</button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t('Room contains unknown devices')}
|
title={_t('Room contains unknown devices')}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<h4>
|
<h4>
|
||||||
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -212,15 +200,10 @@ export default React.createClass({
|
||||||
{ _t("Unknown devices") }:
|
{ _t("Unknown devices") }:
|
||||||
|
|
||||||
<UnknownDeviceList devices={this.props.devices} />
|
<UnknownDeviceList devices={this.props.devices} />
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbarWrapper>
|
||||||
<div className="mx_Dialog_buttons">
|
<DialogButtons primaryButton={sendButtonLabel}
|
||||||
{sendButton}
|
onPrimaryButtonClick={sendButtonOnClick}
|
||||||
<button className="mx_Dialog_primary" autoFocus={true}
|
onCancel={this._onDismissClicked} />
|
||||||
onClick={this._onDismissClicked}
|
|
||||||
>
|
|
||||||
{_t("Dismiss")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
|
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { KeyCode } from '../../../Keyboard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||||
|
@ -27,8 +30,34 @@ import React from 'react';
|
||||||
export default function AccessibleButton(props) {
|
export default function AccessibleButton(props) {
|
||||||
const {element, onClick, children, ...restProps} = props;
|
const {element, onClick, children, ...restProps} = props;
|
||||||
restProps.onClick = onClick;
|
restProps.onClick = onClick;
|
||||||
|
// We need to consume enter onKeyDown and space onKeyUp
|
||||||
|
// otherwise we are risking also activating other keyboard focusable elements
|
||||||
|
// that might receive focus as a result of the AccessibleButtonClick action
|
||||||
|
// It's because we are using html buttons at a few places e.g. inside dialogs
|
||||||
|
// And divs which we report as role button to assistive technologies.
|
||||||
|
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||||
|
// inconsistencies here
|
||||||
|
restProps.onKeyDown = function(e) {
|
||||||
|
if (e.keyCode === KeyCode.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
return onClick(e);
|
||||||
|
}
|
||||||
|
if (e.keyCode === KeyCode.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
restProps.onKeyUp = function(e) {
|
restProps.onKeyUp = function(e) {
|
||||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
if (e.keyCode === KeyCode.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
return onClick(e);
|
||||||
|
}
|
||||||
|
if (e.keyCode === KeyCode.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
restProps.tabIndex = restProps.tabIndex || "0";
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
restProps.role = "button";
|
restProps.role = "button";
|
||||||
|
@ -44,9 +73,9 @@ export default function AccessibleButton(props) {
|
||||||
* implemented exactly like a normal onClick handler.
|
* implemented exactly like a normal onClick handler.
|
||||||
*/
|
*/
|
||||||
AccessibleButton.propTypes = {
|
AccessibleButton.propTypes = {
|
||||||
children: React.PropTypes.node,
|
children: PropTypes.node,
|
||||||
element: React.PropTypes.string,
|
element: PropTypes.string,
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
AccessibleButton.defaultProps = {
|
AccessibleButton.defaultProps = {
|
||||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { UserAddressType } from '../../../UserAddress';
|
import { UserAddressType } from '../../../UserAddress';
|
||||||
|
@ -26,17 +27,17 @@ export default React.createClass({
|
||||||
displayName: 'AddressSelector',
|
displayName: 'AddressSelector',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onSelected: React.PropTypes.func.isRequired,
|
onSelected: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// List of the addresses to display
|
// List of the addresses to display
|
||||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
|
||||||
// Whether to show the address on the address tiles
|
// Whether to show the address on the address tiles
|
||||||
showAddress: React.PropTypes.bool,
|
showAddress: PropTypes.bool,
|
||||||
truncateAt: React.PropTypes.number.isRequired,
|
truncateAt: PropTypes.number.isRequired,
|
||||||
selected: React.PropTypes.number,
|
selected: PropTypes.number,
|
||||||
|
|
||||||
// Element to put as a header on top of the list
|
// Element to put as a header on top of the list
|
||||||
header: React.PropTypes.node,
|
header: PropTypes.node,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import sdk from "../../../index";
|
import sdk from "../../../index";
|
||||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
@ -28,9 +29,9 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
address: UserAddressType.isRequired,
|
address: UserAddressType.isRequired,
|
||||||
canDismiss: React.PropTypes.bool,
|
canDismiss: PropTypes.bool,
|
||||||
onDismissed: React.PropTypes.func,
|
onDismissed: PropTypes.func,
|
||||||
justified: React.PropTypes.bool,
|
justified: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/**
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import qs from 'querystring';
|
import qs from 'querystring';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
@ -35,32 +36,25 @@ import WidgetUtils from '../../../WidgetUtils';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
|
const ENABLE_REACT_PERF = false;
|
||||||
|
|
||||||
export default React.createClass({
|
export default class AppTile extends React.Component {
|
||||||
displayName: 'AppTile',
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = this._getNewState(props);
|
||||||
|
|
||||||
propTypes: {
|
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||||
id: React.PropTypes.string.isRequired,
|
this._onMessage = this._onMessage.bind(this);
|
||||||
url: React.PropTypes.string.isRequired,
|
this._onLoaded = this._onLoaded.bind(this);
|
||||||
name: React.PropTypes.string.isRequired,
|
this._onEditClick = this._onEditClick.bind(this);
|
||||||
room: React.PropTypes.object.isRequired,
|
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||||
type: React.PropTypes.string.isRequired,
|
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||||
fullWidth: React.PropTypes.bool,
|
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||||
// UserId of the current user
|
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||||
userId: React.PropTypes.string.isRequired,
|
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||||
// UserId of the entity that added / modified the widget
|
}
|
||||||
creatorUserId: React.PropTypes.string,
|
|
||||||
waitForIframeLoad: React.PropTypes.bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps() {
|
|
||||||
return {
|
|
||||||
url: "",
|
|
||||||
waitForIframeLoad: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||||
|
@ -72,8 +66,8 @@ export default React.createClass({
|
||||||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||||
widgetUrl: this._addWurlParams(newProps.url),
|
widgetUrl: this._addWurlParams(newProps.url),
|
||||||
widgetPermissionId: widgetPermissionId,
|
widgetPermissionId: widgetPermissionId,
|
||||||
// Assume that widget has permission to load if we are the user who
|
// Assume that widget has permission to load if we are the user who
|
||||||
|
@ -82,8 +76,20 @@ export default React.createClass({
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
widgetPageTitle: newProps.widgetPageTitle,
|
widgetPageTitle: newProps.widgetPageTitle,
|
||||||
|
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
|
||||||
|
this.props.whitelistCapabilities : [],
|
||||||
|
requestedCapabilities: [],
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the widget support a given capability
|
||||||
|
* @param {[type]} capability Capability to check for
|
||||||
|
* @return {Boolean} True if capability supported
|
||||||
|
*/
|
||||||
|
_hasCapability(capability) {
|
||||||
|
return this.state.allowedCapabilities.some((c) => {return c === capability;});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add widget instance specific parameters to pass in wUrl
|
* Add widget instance specific parameters to pass in wUrl
|
||||||
|
@ -111,11 +117,7 @@ export default React.createClass({
|
||||||
u.query = params;
|
u.query = params;
|
||||||
|
|
||||||
return u.format();
|
return u.format();
|
||||||
},
|
}
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return this._getNewState(this.props);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||||
|
@ -139,7 +141,7 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
isMixedContent() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
|
@ -151,14 +153,36 @@ export default React.createClass({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
WidgetMessaging.startListening();
|
|
||||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
|
||||||
window.addEventListener('message', this._onMessage, false);
|
|
||||||
this.setScalarToken();
|
this.setScalarToken();
|
||||||
},
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// Legacy Jitsi widget messaging -- TODO replace this with standard widget
|
||||||
|
// postMessaging API
|
||||||
|
window.addEventListener('message', this._onMessage, false);
|
||||||
|
|
||||||
|
// Widget action listeners
|
||||||
|
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// Widget action listeners
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
|
||||||
|
// Widget postMessage listeners
|
||||||
|
try {
|
||||||
|
if (this.widgetMessaging) {
|
||||||
|
this.widgetMessaging.stop();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||||
|
}
|
||||||
|
// Jitsi listener
|
||||||
|
window.removeEventListener('message', this._onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a scalar token to the widget URL, if required
|
* Adds a scalar token to the widget URL, if required
|
||||||
|
@ -210,13 +234,7 @@ export default React.createClass({
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
WidgetMessaging.stopListening();
|
|
||||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
|
||||||
window.removeEventListener('message', this._onMessage);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.url !== this.props.url) {
|
if (nextProps.url !== this.props.url) {
|
||||||
|
@ -231,8 +249,10 @@ export default React.createClass({
|
||||||
widgetPageTitle: nextProps.widgetPageTitle,
|
widgetPageTitle: nextProps.widgetPageTitle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// Legacy Jitsi widget messaging
|
||||||
|
// TODO -- This should be replaced with the new widget postMessaging API
|
||||||
_onMessage(event) {
|
_onMessage(event) {
|
||||||
if (this.props.type !== 'jitsi') {
|
if (this.props.type !== 'jitsi') {
|
||||||
return;
|
return;
|
||||||
|
@ -250,63 +270,140 @@ export default React.createClass({
|
||||||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_canUserModify() {
|
_canUserModify() {
|
||||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onEditClick(e) {
|
_onEditClick(e) {
|
||||||
console.log("Edit widget ID ", this.props.id);
|
console.log("Edit widget ID ", this.props.id);
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
if (this.props.onEditClick) {
|
||||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
this.props.onEditClick();
|
||||||
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
|
} else {
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
src: src,
|
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||||
}, "mx_IntegrationsManager");
|
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||||
},
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
|
src: src,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSnapshotClick(e) {
|
||||||
|
console.warn("Requesting widget snapshot");
|
||||||
|
this.widgetMessaging.getScreenshot()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to get screenshot", err);
|
||||||
|
})
|
||||||
|
.then((screenshot) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'picture_snapshot',
|
||||||
|
file: screenshot,
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* If user has permission to modify widgets, delete the widget,
|
/* If user has permission to modify widgets, delete the widget,
|
||||||
* otherwise revoke access for the widget to load in the user's browser
|
* otherwise revoke access for the widget to load in the user's browser
|
||||||
*/
|
*/
|
||||||
_onDeleteClick() {
|
_onDeleteClick() {
|
||||||
if (this._canUserModify()) {
|
if (this.props.onDeleteClick) {
|
||||||
// Show delete confirmation dialog
|
this.props.onDeleteClick();
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
|
||||||
title: _t("Delete Widget"),
|
|
||||||
description: _t(
|
|
||||||
"Deleting a widget removes it for all users in this room." +
|
|
||||||
" Are you sure you want to delete this widget?"),
|
|
||||||
button: _t("Delete widget"),
|
|
||||||
onFinished: (confirmed) => {
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({deleting: true});
|
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
|
||||||
this.props.room.roomId,
|
|
||||||
'im.vector.modular.widgets',
|
|
||||||
{}, // empty content
|
|
||||||
this.props.id,
|
|
||||||
).catch((e) => {
|
|
||||||
console.error('Failed to delete widget', e);
|
|
||||||
this.setState({deleting: false});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Revoke widget permissions - %s", this.props.id);
|
if (this._canUserModify()) {
|
||||||
this._revokeWidgetPermission();
|
// Show delete confirmation dialog
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||||
|
title: _t("Delete Widget"),
|
||||||
|
description: _t(
|
||||||
|
"Deleting a widget removes it for all users in this room." +
|
||||||
|
" Are you sure you want to delete this widget?"),
|
||||||
|
button: _t("Delete widget"),
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({deleting: true});
|
||||||
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
|
this.props.room.roomId,
|
||||||
|
'im.vector.modular.widgets',
|
||||||
|
{}, // empty content
|
||||||
|
this.props.id,
|
||||||
|
).catch((e) => {
|
||||||
|
console.error('Failed to delete widget', e);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({deleting: false});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Revoke widget permissions - %s", this.props.id);
|
||||||
|
this._revokeWidgetPermission();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when widget iframe has finished loading
|
* Called when widget iframe has finished loading
|
||||||
*/
|
*/
|
||||||
_onLoaded() {
|
_onLoaded() {
|
||||||
|
if (!this.widgetMessaging) {
|
||||||
|
this._onInitialLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on initial load of the widget iframe
|
||||||
|
*/
|
||||||
|
_onInitialLoad() {
|
||||||
|
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||||
|
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||||
|
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||||
|
requestedCapabilities = requestedCapabilities || [];
|
||||||
|
|
||||||
|
// Allow whitelisted capabilities
|
||||||
|
let requestedWhitelistCapabilies = [];
|
||||||
|
|
||||||
|
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
||||||
|
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
||||||
|
return this.indexOf(e)>=0;
|
||||||
|
}, this.props.whitelistCapabilities);
|
||||||
|
|
||||||
|
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||||
|
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
|
||||||
|
requestedWhitelistCapabilies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||||
|
this.setState({
|
||||||
|
requestedCapabilities,
|
||||||
|
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.props.onCapabilityRequest) {
|
||||||
|
this.props.onCapabilityRequest(requestedCapabilities);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
||||||
|
});
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
},
|
}
|
||||||
|
|
||||||
|
_onWidgetAction(payload) {
|
||||||
|
if (payload.widgetId === this.props.id) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'm.sticker':
|
||||||
|
if (this._hasCapability('m.sticker')) {
|
||||||
|
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||||
|
} else {
|
||||||
|
console.warn('Ignoring sticker message. Invalid capability');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set remote content title on AppTile
|
* Set remote content title on AppTile
|
||||||
|
@ -320,7 +417,7 @@ export default React.createClass({
|
||||||
}, (err) =>{
|
}, (err) =>{
|
||||||
console.error("Failed to get page title", err);
|
console.error("Failed to get page title", err);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
// Widget labels to render, depending upon user permissions
|
// Widget labels to render, depending upon user permissions
|
||||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||||
|
@ -329,20 +426,20 @@ export default React.createClass({
|
||||||
return _td('Delete widget');
|
return _td('Delete widget');
|
||||||
}
|
}
|
||||||
return _td('Revoke widget access');
|
return _td('Revoke widget access');
|
||||||
},
|
}
|
||||||
|
|
||||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||||
_grantWidgetPermission() {
|
_grantWidgetPermission() {
|
||||||
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||||
localStorage.setItem(this.state.widgetPermissionId, true);
|
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||||
this.setState({hasPermissionToLoad: true});
|
this.setState({hasPermissionToLoad: true});
|
||||||
},
|
}
|
||||||
|
|
||||||
_revokeWidgetPermission() {
|
_revokeWidgetPermission() {
|
||||||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||||
localStorage.removeItem(this.state.widgetPermissionId);
|
localStorage.removeItem(this.state.widgetPermissionId);
|
||||||
this.setState({hasPermissionToLoad: false});
|
this.setState({hasPermissionToLoad: false});
|
||||||
},
|
}
|
||||||
|
|
||||||
formatAppTileName() {
|
formatAppTileName() {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
|
@ -350,7 +447,7 @@ export default React.createClass({
|
||||||
appTileName = this.props.name.trim();
|
appTileName = this.props.name.trim();
|
||||||
}
|
}
|
||||||
return appTileName;
|
return appTileName;
|
||||||
},
|
}
|
||||||
|
|
||||||
onClickMenuBar(ev) {
|
onClickMenuBar(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -365,16 +462,42 @@ export default React.createClass({
|
||||||
action: 'appsDrawer',
|
action: 'appsDrawer',
|
||||||
show: !this.props.show,
|
show: !this.props.show,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_getSafeUrl() {
|
_getSafeUrl() {
|
||||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
|
||||||
|
if (ENABLE_REACT_PERF) {
|
||||||
|
parsedWidgetUrl.search = null;
|
||||||
|
parsedWidgetUrl.query.react_perf = true;
|
||||||
|
}
|
||||||
let safeWidgetUrl = '';
|
let safeWidgetUrl = '';
|
||||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
}
|
}
|
||||||
return safeWidgetUrl;
|
return safeWidgetUrl;
|
||||||
},
|
}
|
||||||
|
|
||||||
|
_getTileTitle() {
|
||||||
|
const name = this.formatAppTileName();
|
||||||
|
const titleSpacer = <span> - </span>;
|
||||||
|
let title = '';
|
||||||
|
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
||||||
|
title = this.state.widgetPageTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<b>{ name }</b>
|
||||||
|
<span>{ title ? titleSpacer : '' }{ title }</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMinimiseClick(e) {
|
||||||
|
if (this.props.onMinimiseClick) {
|
||||||
|
this.props.onMinimiseClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
@ -392,9 +515,13 @@ export default React.createClass({
|
||||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||||
"allow-same-origin allow-scripts allow-presentation";
|
"allow-same-origin allow-scripts allow-presentation";
|
||||||
|
|
||||||
|
// Additional iframe feature pemissions
|
||||||
|
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||||
|
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||||
|
|
||||||
if (this.props.show) {
|
if (this.props.show) {
|
||||||
const loadingElement = (
|
const loadingElement = (
|
||||||
<div className='mx_AppTileBody mx_AppLoading'>
|
<div>
|
||||||
<MessageSpinner msg='Loading...' />
|
<MessageSpinner msg='Loading...' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -409,9 +536,15 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
|
{ /*
|
||||||
|
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||||
|
"allow" attribute, which is unknown to react 15.
|
||||||
|
*/ }
|
||||||
<iframe
|
<iframe
|
||||||
|
is
|
||||||
|
allow={iframeFeatures}
|
||||||
ref="appFrame"
|
ref="appFrame"
|
||||||
src={this._getSafeUrl()}
|
src={this._getSafeUrl()}
|
||||||
allowFullScreen="true"
|
allowFullScreen="true"
|
||||||
|
@ -445,29 +578,42 @@ export default React.createClass({
|
||||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Picture snapshot - only show button when apps are maximised.
|
||||||
|
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
|
||||||
|
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
|
{ this.props.showMenubar &&
|
||||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
<span className="mx_AppTileMenuBarTitle">
|
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||||
<TintableSvgButton
|
{ this.props.showMinimise && <TintableSvgButton
|
||||||
src={windowStateIcon}
|
src={windowStateIcon}
|
||||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
title={_t('Minimize apps')}
|
title={_t('Minimize apps')}
|
||||||
width="10"
|
width="10"
|
||||||
height="10"
|
height="10"
|
||||||
/>
|
onClick={this._onMinimiseClick}
|
||||||
<b>{ this.formatAppTileName() }</b>
|
/> }
|
||||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
{ this.props.showTitle && this._getTileTitle() }
|
||||||
<span> - { this.state.widgetPageTitle }</span>
|
|
||||||
) }
|
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
|
{ /* Snapshot widget */ }
|
||||||
|
{ showPictureSnapshotButton && <TintableSvgButton
|
||||||
|
src={showPictureSnapshotIcon}
|
||||||
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
|
title={_t('Picture')}
|
||||||
|
onClick={this._onSnapshotClick}
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
/> }
|
||||||
|
|
||||||
{ /* Edit widget */ }
|
{ /* Edit widget */ }
|
||||||
{ showEditButton && <TintableSvgButton
|
{ showEditButton && <TintableSvgButton
|
||||||
src="img/edit_green.svg"
|
src="img/edit_green.svg"
|
||||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className={"mx_AppTileMenuBarWidget " +
|
||||||
|
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
|
||||||
title={_t('Edit')}
|
title={_t('Edit')}
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
width="10"
|
width="10"
|
||||||
|
@ -475,18 +621,71 @@ export default React.createClass({
|
||||||
/> }
|
/> }
|
||||||
|
|
||||||
{ /* Delete widget */ }
|
{ /* Delete widget */ }
|
||||||
<TintableSvgButton
|
{ this.props.showDelete && <TintableSvgButton
|
||||||
src={deleteIcon}
|
src={deleteIcon}
|
||||||
className={deleteClasses}
|
className={deleteClasses}
|
||||||
title={_t(deleteWidgetLabel)}
|
title={_t(deleteWidgetLabel)}
|
||||||
onClick={this._onDeleteClick}
|
onClick={this._onDeleteClick}
|
||||||
width="10"
|
width="10"
|
||||||
height="10"
|
height="10"
|
||||||
/>
|
/> }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div> }
|
||||||
{ appTileBody }
|
{ appTileBody }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
AppTile.displayName ='AppTile';
|
||||||
|
|
||||||
|
AppTile.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
room: PropTypes.object.isRequired,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
|
fullWidth: PropTypes.bool,
|
||||||
|
// UserId of the current user
|
||||||
|
userId: PropTypes.string.isRequired,
|
||||||
|
// UserId of the entity that added / modified the widget
|
||||||
|
creatorUserId: PropTypes.string,
|
||||||
|
waitForIframeLoad: PropTypes.bool,
|
||||||
|
showMenubar: PropTypes.bool,
|
||||||
|
// Should the AppTile render itself
|
||||||
|
show: PropTypes.bool,
|
||||||
|
// Optional onEditClickHandler (overrides default behaviour)
|
||||||
|
onEditClick: PropTypes.func,
|
||||||
|
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||||
|
onDeleteClick: PropTypes.func,
|
||||||
|
// Optional onMinimiseClickHandler
|
||||||
|
onMinimiseClick: PropTypes.func,
|
||||||
|
// Optionally hide the tile title
|
||||||
|
showTitle: PropTypes.bool,
|
||||||
|
// Optionally hide the tile minimise icon
|
||||||
|
showMinimise: PropTypes.bool,
|
||||||
|
// Optionally handle minimise button pointer events (default false)
|
||||||
|
handleMinimisePointerEvents: PropTypes.bool,
|
||||||
|
// Optionally hide the delete icon
|
||||||
|
showDelete: PropTypes.bool,
|
||||||
|
// Widget apabilities to allow by default (without user confirmation)
|
||||||
|
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||||
|
// basic widget capabilities, e.g. injecting sticker message events.
|
||||||
|
whitelistCapabilities: PropTypes.array,
|
||||||
|
// Optional function to be called on widget capability request
|
||||||
|
// Called with an array of the requested capabilities
|
||||||
|
onCapabilityRequest: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
AppTile.defaultProps = {
|
||||||
|
url: "",
|
||||||
|
waitForIframeLoad: true,
|
||||||
|
showMenubar: true,
|
||||||
|
showTitle: true,
|
||||||
|
showMinimise: true,
|
||||||
|
showDelete: true,
|
||||||
|
handleMinimisePointerEvents: false,
|
||||||
|
whitelistCapabilities: [],
|
||||||
|
};
|
||||||
|
|
|
@ -15,71 +15,30 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DragSource, DropTarget } from 'react-dnd';
|
|
||||||
|
|
||||||
import TagTile from './TagTile';
|
import TagTile from './TagTile';
|
||||||
import dis from '../../../dispatcher';
|
|
||||||
import { findDOMNode } from 'react-dom';
|
|
||||||
|
|
||||||
const tagTileSource = {
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
canDrag: function(props, monitor) {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
beginDrag: function(props) {
|
export default function DNDTagTile(props) {
|
||||||
// Return the data describing the dragged item
|
return <div>
|
||||||
return {
|
<Draggable
|
||||||
tag: props.groupProfile.groupId,
|
key={props.tag}
|
||||||
};
|
draggableId={props.tag}
|
||||||
},
|
index={props.index}
|
||||||
|
type="draggable-TagTile"
|
||||||
endDrag: function(props, monitor, component) {
|
>
|
||||||
const dropResult = monitor.getDropResult();
|
{ (provided, snapshot) => (
|
||||||
if (!monitor.didDrop() || !dropResult) {
|
<div>
|
||||||
return;
|
<div
|
||||||
}
|
ref={provided.innerRef}
|
||||||
props.onEndDrag();
|
{...provided.draggableProps}
|
||||||
},
|
{...provided.dragHandleProps}
|
||||||
};
|
>
|
||||||
|
<TagTile {...props} />
|
||||||
const tagTileTarget = {
|
</div>
|
||||||
canDrop(props, monitor) {
|
{ provided.placeholder }
|
||||||
return true;
|
</div>
|
||||||
},
|
) }
|
||||||
|
</Draggable>
|
||||||
hover(props, monitor, component) {
|
</div>;
|
||||||
if (!monitor.canDrop()) return;
|
}
|
||||||
const draggedY = monitor.getClientOffset().y;
|
|
||||||
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
|
||||||
const targetY = (top + bottom) / 2;
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'order_tag',
|
|
||||||
tag: monitor.getItem().tag,
|
|
||||||
targetTag: props.groupProfile.groupId,
|
|
||||||
// Note: we indicate that the tag should be after the target when
|
|
||||||
// it's being dragged over the top half of the target.
|
|
||||||
after: draggedY < targetY,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
drop(props) {
|
|
||||||
// Return the data to be returned by getDropResult
|
|
||||||
return {
|
|
||||||
tag: props.groupProfile.groupId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default
|
|
||||||
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
|
||||||
connectDropTarget: connect.dropTarget(),
|
|
||||||
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
|
||||||
connectDragSource: connect.dragSource(),
|
|
||||||
}))((props) => {
|
|
||||||
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
|
||||||
return connectDropTarget(connectDragSource(
|
|
||||||
<div>
|
|
||||||
<TagTile {...otherProps} />
|
|
||||||
</div>,
|
|
||||||
));
|
|
||||||
}));
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -24,8 +25,8 @@ export default React.createClass({
|
||||||
displayName: 'DeviceVerifyButtons',
|
displayName: 'DeviceVerifyButtons',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
device: React.PropTypes.object.isRequired,
|
device: PropTypes.object.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
62
src/components/views/elements/DialogButtons.js
Normal file
62
src/components/views/elements/DialogButtons.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Aidan Gauland
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic container for buttons in modal dialogs.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: "DialogButtons",
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// The primary button which is styled differently and has default focus.
|
||||||
|
primaryButton: PropTypes.node.isRequired,
|
||||||
|
|
||||||
|
// onClick handler for the primary button.
|
||||||
|
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// onClick handler for the cancel button.
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
focus: PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
let primaryButtonClassName = "mx_Dialog_primary";
|
||||||
|
if (this.props.primaryButtonClass) {
|
||||||
|
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button className={primaryButtonClassName}
|
||||||
|
onClick={this.props.onPrimaryButtonClick}
|
||||||
|
autoFocus={this.props.focus}
|
||||||
|
>
|
||||||
|
{ this.props.primaryButton }
|
||||||
|
</button>
|
||||||
|
{ this.props.children }
|
||||||
|
<button onClick={this.props.onCancel}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
export default class DirectorySearchBox extends React.Component {
|
export default class DirectorySearchBox extends React.Component {
|
||||||
|
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectorySearchBox.propTypes = {
|
DirectorySearchBox.propTypes = {
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onClear: React.PropTypes.func,
|
onClear: PropTypes.func,
|
||||||
onJoinClick: React.PropTypes.func,
|
onJoinClick: PropTypes.func,
|
||||||
placeholder: React.PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
showJoinButton: React.PropTypes.bool,
|
showJoinButton: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuOption.propTypes = {
|
MenuOption.propTypes = {
|
||||||
children: React.PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
React.PropTypes.arrayOf(React.PropTypes.node),
|
PropTypes.arrayOf(React.PropTypes.node),
|
||||||
React.PropTypes.node,
|
PropTypes.node,
|
||||||
]),
|
]),
|
||||||
highlighted: React.PropTypes.bool,
|
highlighted: PropTypes.bool,
|
||||||
dropdownKey: React.PropTypes.string,
|
dropdownKey: PropTypes.string,
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
onMouseEnter: React.PropTypes.func.isRequired,
|
onMouseEnter: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -322,20 +323,20 @@ Dropdown.propTypes = {
|
||||||
// The width that the dropdown should be. If specified,
|
// The width that the dropdown should be. If specified,
|
||||||
// the dropped-down part of the menu will be set to this
|
// the dropped-down part of the menu will be set to this
|
||||||
// width.
|
// width.
|
||||||
menuWidth: React.PropTypes.number,
|
menuWidth: PropTypes.number,
|
||||||
// Called when the selected option changes
|
// Called when the selected option changes
|
||||||
onOptionChange: React.PropTypes.func.isRequired,
|
onOptionChange: PropTypes.func.isRequired,
|
||||||
// Called when the value of the search field changes
|
// Called when the value of the search field changes
|
||||||
onSearchChange: React.PropTypes.func,
|
onSearchChange: PropTypes.func,
|
||||||
searchEnabled: React.PropTypes.bool,
|
searchEnabled: PropTypes.bool,
|
||||||
// Function that, given the key of an option, returns
|
// Function that, given the key of an option, returns
|
||||||
// a node representing that option to be displayed in the
|
// a node representing that option to be displayed in the
|
||||||
// box itself as the currently-selected option (ie. as
|
// box itself as the currently-selected option (ie. as
|
||||||
// opposed to in the actual dropped-down part). If
|
// opposed to in the actual dropped-down part). If
|
||||||
// unspecified, the appropriate child element is used as
|
// unspecified, the appropriate child element is used as
|
||||||
// in the dropped-down menu.
|
// in the dropped-down menu.
|
||||||
getShortOption: React.PropTypes.func,
|
getShortOption: PropTypes.func,
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
// negative for consistency with HTML
|
// negative for consistency with HTML
|
||||||
disabled: React.PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const KEY_TAB = 9;
|
const KEY_TAB = 9;
|
||||||
const KEY_SHIFT = 16;
|
const KEY_SHIFT = 16;
|
||||||
|
@ -26,18 +27,18 @@ module.exports = React.createClass({
|
||||||
displayName: 'EditableText',
|
displayName: 'EditableText',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onValueChanged: React.PropTypes.func,
|
onValueChanged: PropTypes.func,
|
||||||
initialValue: React.PropTypes.string,
|
initialValue: PropTypes.string,
|
||||||
label: React.PropTypes.string,
|
label: PropTypes.string,
|
||||||
placeholder: React.PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
labelClassName: React.PropTypes.string,
|
labelClassName: PropTypes.string,
|
||||||
placeholderClassName: React.PropTypes.string,
|
placeholderClassName: PropTypes.string,
|
||||||
// Overrides blurToSubmit if true
|
// Overrides blurToSubmit if true
|
||||||
blurToCancel: React.PropTypes.bool,
|
blurToCancel: PropTypes.bool,
|
||||||
// Will cause onValueChanged(value, true) to fire on blur
|
// Will cause onValueChanged(value, true) to fire on blur
|
||||||
blurToSubmit: React.PropTypes.bool,
|
blurToSubmit: PropTypes.bool,
|
||||||
editable: React.PropTypes.bool,
|
editable: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
Phases: {
|
Phases: {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
|
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
|
||||||
|
|
||||||
EditableTextContainer.propTypes = {
|
EditableTextContainer.propTypes = {
|
||||||
/* callback to retrieve the initial value. */
|
/* callback to retrieve the initial value. */
|
||||||
getInitialValue: React.PropTypes.func,
|
getInitialValue: PropTypes.func,
|
||||||
|
|
||||||
/* initial value; used if getInitialValue is not given */
|
/* initial value; used if getInitialValue is not given */
|
||||||
initialValue: React.PropTypes.string,
|
initialValue: PropTypes.string,
|
||||||
|
|
||||||
/* placeholder text to use when the value is empty (and not being
|
/* placeholder text to use when the value is empty (and not being
|
||||||
* edited) */
|
* edited) */
|
||||||
placeholder: React.PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
|
|
||||||
/* callback to update the value. Called with a single argument: the new
|
/* callback to update the value. Called with a single argument: the new
|
||||||
* value. */
|
* value. */
|
||||||
onSubmit: React.PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
|
|
||||||
/* should the input submit when focus is lost? */
|
/* should the input submit when focus is lost? */
|
||||||
blurToSubmit: React.PropTypes.bool,
|
blurToSubmit: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||||
|
|
||||||
export default function EmojiText(props) {
|
export default function EmojiText(props) {
|
||||||
|
@ -32,8 +33,8 @@ export default function EmojiText(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiText.propTypes = {
|
EmojiText.propTypes = {
|
||||||
element: React.PropTypes.string,
|
element: PropTypes.string,
|
||||||
children: React.PropTypes.string.isRequired,
|
children: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
EmojiText.defaultProps = {
|
EmojiText.defaultProps = {
|
||||||
|
|
|
@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
FlairAvatar.contextTypes = {
|
FlairAvatar.contextTypes = {
|
||||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Flair extends React.Component {
|
export default class Flair extends React.Component {
|
||||||
|
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
|
||||||
}
|
}
|
||||||
const profiles = await this._getGroupProfiles(groups);
|
const profiles = await this._getGroupProfiles(groups);
|
||||||
if (!this.unmounted) {
|
if (!this.unmounted) {
|
||||||
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
|
this.setState({
|
||||||
|
profiles: profiles.filter((profile) => {
|
||||||
|
return profile ? profile.avatarUrl : false;
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,5 +138,5 @@ Flair.propTypes = {
|
||||||
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
||||||
// See https://github.com/vector-im/riot-web/issues/4951.
|
// See https://github.com/vector-im/riot-web/issues/4951.
|
||||||
Flair.contextTypes = {
|
Flair.contextTypes = {
|
||||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue