From adf186f568037b18e0ae9302d639c1a39dbe0224 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Jun 2020 23:02:10 +0100 Subject: [PATCH 01/35] Fix RoomTile2 Context Menu to match Figma Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomTile2.scss | 8 ++++---- src/components/views/rooms/RoomTile2.tsx | 24 +++++++++++------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 2845068de3..f1516ee0e3 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -118,8 +118,7 @@ limitations under the License. } .mx_RoomTile2_menuButton::before { - top: 8px; - left: -1px; // this is off-center to align it with the badges + left: 1px; // this is off-center to align it with the badges mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); } @@ -133,9 +132,10 @@ limitations under the License. } .mx_RoomTile2_menuButton { - width: 18px; - height: 32px; + width: 16px; + height: 16px; visibility: visible; + margin: auto 0; } } } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 1284728855..59741635f1 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -64,8 +64,14 @@ interface IState { generalMenuDisplayed: boolean; } +export const contextMenuBelow = (elementRect) => { + const left = elementRect.left + window.pageXOffset - 6; + let top = elementRect.bottom + window.pageYOffset + 21; + const chevronFace = "none"; + return {left, top, chevronFace}; +}; + export default class RoomTile2 extends React.Component { - private roomTileRef: React.RefObject = createRef(); private generalMenuButtonRef: React.RefObject = createRef(); // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180 @@ -159,18 +165,10 @@ export default class RoomTile2 extends React.Component { let contextMenu = null; if (this.state.generalMenuDisplayed) { // The context menu appears within the list, so use the room tile as a reference point - const elementRect = this.roomTileRef.current.getBoundingClientRect(); + const elementRect = this.generalMenuButtonRef.current.getBoundingClientRect(); contextMenu = ( - -
+ +
  • @@ -280,7 +278,7 @@ export default class RoomTile2 extends React.Component { const avatarSize = 32; return ( - + {({onFocus, isActive, ref}) => Date: Tue, 30 Jun 2020 00:16:51 +0100 Subject: [PATCH 02/35] Add room notifications context menu and non-default indicator to RoomTile2 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomTile2.scss | 37 ++++++---- src/components/views/rooms/RoomTile2.tsx | 87 +++++++++++++++++++++++- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index f1516ee0e3..78a7732882 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -92,20 +92,17 @@ limitations under the License. justify-content: center; } - // The menu button is hidden by default - // TODO: [Notifications] Use mx_RoomTile2_notificationsButton, similar to the following approach: - // https://github.com/matrix-org/matrix-react-sdk/blob/2180a56074f3698fc0241c309a72ba6cad802d1c/res/css/views/rooms/_RoomSublist2.scss#L48-L76 - // You'll need to do the same down below on the &:hover selector for the tile. - // See https://github.com/vector-im/riot-web/issues/13961. - // ... also remove this 5 line TODO comment. .mx_RoomTile2_menuButton, .mx_RoomTile2_notificationsButton { - width: 0; - height: 0; - visibility: hidden; + width: 20px; + height: 20px; + margin: auto 0 auto 8px; position: relative; + display: none; &::before { + top: 2px; + left: 2px; content: ''; width: 16px; height: 16px; @@ -117,8 +114,11 @@ limitations under the License. } } + .mx_RoomTile2_notificationsButton.mx_RoomTile2_notificationsButton_show { + display: block; + } + .mx_RoomTile2_menuButton::before { - left: 1px; // this is off-center to align it with the badges mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); } @@ -131,11 +131,9 @@ limitations under the License. visibility: hidden; } + .mx_RoomTile2_notificationsButton, .mx_RoomTile2_menuButton { - width: 16px; - height: 16px; - visibility: visible; - margin: auto 0; + display: block; } } } @@ -158,6 +156,17 @@ limitations under the License. } } +// We use these both in context menus and the room tiles +.mx_RoomTile2_iconBell::before { + mask-image: url('$(res)/img/feather-customised/bell.svg'); +} +.mx_RoomTile2_iconBellDot::before { + mask-image: url('$(res)/img/feather-customised/bell-notification.custom.svg'); +} +.mx_RoomTile2_iconBellCrossed::before { + mask-image: url('$(res)/img/feather-customised/bell-crossed.svg'); +} + .mx_RoomTile2_contextMenu { .mx_RoomTile2_contextMenu_redRow { .mx_AccessibleButton { diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 59741635f1..52b3d444ac 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -36,6 +36,7 @@ import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomTileIcon from "./RoomTileIcon"; +import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -61,6 +62,7 @@ interface IState { hover: boolean; notificationState: INotificationState; selected: boolean; + notificationsMenuDisplayed: boolean; generalMenuDisplayed: boolean; } @@ -72,6 +74,7 @@ export const contextMenuBelow = (elementRect) => { }; export default class RoomTile2 extends React.Component { + private notificationsMenuButtonRef: React.RefObject = createRef(); private generalMenuButtonRef: React.RefObject = createRef(); // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180 @@ -83,6 +86,7 @@ export default class RoomTile2 extends React.Component { hover: false, notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, + notificationsMenuDisplayed: false, generalMenuDisplayed: false, }; @@ -117,6 +121,18 @@ export default class RoomTile2 extends React.Component { this.setState({selected: isActive}); }; + private onNotificationsMenuOpenClick = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({notificationsMenuDisplayed: true}); + }; + + private onCloseNotificationsMenu = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({notificationsMenuDisplayed: false}); + }; + private onGeneralMenuOpenClick = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -159,12 +175,78 @@ export default class RoomTile2 extends React.Component { this.setState({generalMenuDisplayed: false}); // hide the menu }; + private renderNotificationsMenu(): React.ReactElement { + if (this.props.isMinimized) return null; // no menu when minimized + + let contextMenu = null; + if (this.state.notificationsMenuDisplayed) { + const elementRect = this.notificationsMenuButtonRef.current.getBoundingClientRect(); + contextMenu = ( + +
    +
    +
      +
    • + + + {_t("All messages")} + +
    • +
    • + + + {_t("Default")} + +
    • +
    • + + + {_t("Mentions & Keywords")} + +
    • +
    • + + + {_t("None")} + +
    • +
    +
    +
    +
    + ); + } + + const state = getRoomNotifsState(this.props.room.roomId); + const classes = classNames("mx_RoomTile2_notificationsButton", { + // Show bell icon for the default case too. + mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES, + mx_RoomTile2_iconBellDot: state === MENTIONS_ONLY, + mx_RoomTile2_iconBellCrossed: state === MUTE, + // XXX: RoomNotifs assumes ALL_MESSAGES is default, this is wrong, + // but cannot be fixed until FTUE Notifications lands. + mx_RoomTile2_notificationsButton_show: state !== ALL_MESSAGES, + }); + + return ( + + + {contextMenu} + + ); + } + private renderGeneralMenu(): React.ReactElement { if (this.props.isMinimized) return null; // no menu when minimized let contextMenu = null; if (this.state.generalMenuDisplayed) { - // The context menu appears within the list, so use the room tile as a reference point const elementRect = this.generalMenuButtonRef.current.getBoundingClientRect(); contextMenu = ( @@ -227,7 +309,7 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile2': true, 'mx_RoomTile2_selected': this.state.selected, - 'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed, + 'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed || this.state.notificationsMenuDisplayed, 'mx_RoomTile2_minimized': this.props.isMinimized, }); @@ -298,6 +380,7 @@ export default class RoomTile2 extends React.Component {
    {badge}
    + {this.renderNotificationsMenu()} {this.renderGeneralMenu()}
    } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 495300f3fe..0ef675c7c8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1218,6 +1218,9 @@ "%(count)s unread messages.|one": "1 unread message.", "Unread mentions.": "Unread mentions.", "Unread messages.": "Unread messages.", + "All messages": "All messages", + "Mentions & Keywords": "Mentions & Keywords", + "Notification options": "Notification options", "Favourite": "Favourite", "Low Priority": "Low Priority", "Leave Room": "Leave Room", @@ -1894,7 +1897,6 @@ "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", "Notification settings": "Notification settings", "All messages (noisy)": "All messages (noisy)", - "All messages": "All messages", "Mentions only": "Mentions only", "Leave": "Leave", "Forget": "Forget", From 6b2ba8caed50ac0c2a9b2871bf40d05d87d31bde Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 30 Jun 2020 00:27:32 +0100 Subject: [PATCH 03/35] Add svgs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/img/feather-customised/bell-crossed.svg | 4 ++++ res/img/feather-customised/bell-notification.custom.svg | 5 +++++ res/img/feather-customised/bell.svg | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 res/img/feather-customised/bell-crossed.svg create mode 100644 res/img/feather-customised/bell-notification.custom.svg create mode 100644 res/img/feather-customised/bell.svg diff --git a/res/img/feather-customised/bell-crossed.svg b/res/img/feather-customised/bell-crossed.svg new file mode 100644 index 0000000000..3ca24662b9 --- /dev/null +++ b/res/img/feather-customised/bell-crossed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/feather-customised/bell-notification.custom.svg b/res/img/feather-customised/bell-notification.custom.svg new file mode 100644 index 0000000000..7bfd551f97 --- /dev/null +++ b/res/img/feather-customised/bell-notification.custom.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/feather-customised/bell.svg b/res/img/feather-customised/bell.svg new file mode 100644 index 0000000000..b6bc5ec502 --- /dev/null +++ b/res/img/feather-customised/bell.svg @@ -0,0 +1,3 @@ + + + From 2a12fd1f6e85d0d201aa9d5e3f439f463930c5bc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 30 Jun 2020 21:58:54 +0100 Subject: [PATCH 04/35] Fix User context menu alignment to match Figma Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 8c06a06852..ef461bf1b4 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -204,7 +204,7 @@ export default class UserMenu extends React.Component { return ( From cbd2a9cd359717556237e8876e9238950e253760 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 30 Jun 2020 22:11:12 +0100 Subject: [PATCH 05/35] Redo Iconized Context Menu styling to match Figma and simplify future a11y work by flattening the DOM Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.scss | 115 +++++++++-------------- res/css/structures/_UserMenu.scss | 4 + src/components/structures/UserMenu.tsx | 78 ++++++--------- src/components/views/rooms/RoomTile2.tsx | 86 ++++++----------- 4 files changed, 107 insertions(+), 176 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 560bd894c6..0d057d96ab 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -588,27 +588,16 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // A context menu that largely fits the | [icon] [label] | format. .mx_IconizedContextMenu { - // Put 20px of padding around the whole menu. We do this instead of a - // simple `padding: 20px` rule so the horizontal rules added by the - // optionLists is rendered correctly (full width). - > * { - padding-left: 20px; - padding-right: 20px; - - &:first-child { - padding-top: 20px; - } - - &:last-child { - padding-bottom: 16px; - } - } + min-width: 146px; .mx_IconizedContextMenu_optionList { + & > * { + padding-left: 20px; + padding-right: 20px; + } + // the notFirst class is for cases where the optionList might be under a header of sorts. &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { - margin-top: 12px; - // This is a bit of a hack when we could just use a simple border-top property, // however we have a (kinda) good reason for doing it this way: we need opacity. // To get the right color, we need an opacity modifier which means we have to work @@ -631,72 +620,54 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } } - ul { - list-style: none; - margin: 0; - padding: 0; + // round the top corners of the top button for the hover effect to be bounded + &:first-child .mx_AccessibleButton:first-child { + border-radius: 4px 4px 0 0; // radius matches .mx_ContextualMenu + } - li { - margin: 0; - padding: 12px 0 0; + // round the bottom corners of the bottom button for the hover effect to be bounded + &:last-child .mx_AccessibleButton:last-child { + border-radius: 0 0 4px 4px; // radius matches .mx_ContextualMenu + } - .mx_AccessibleButton { - text-decoration: none; - color: $primary-fg-color; - font-size: $font-15px; - line-height: $font-24px; + .mx_AccessibleButton { + padding-top: 12px; + padding-bottom: 12px; + text-decoration: none; + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-24px; - // Create a flexbox to more easily define the list items - display: flex; - align-items: center; + // Create a flexbox to more easily define the list items + display: flex; + align-items: center; - img, .mx_IconizedContextMenu_icon { // icons - width: 16px; - min-width: 16px; - max-width: 16px; - } + &:hover { + background-color: $menu-selected-color; + } - span:last-child { // labels - padding-left: 14px; - width: 100%; - flex: 1; + img, .mx_IconizedContextMenu_icon { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - } + span:last-child { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } } } &.mx_IconizedContextMenu_compact { - > * { - padding-left: 11px; - padding-right: 16px; - - &:first-child { - padding-top: 13px; - } - - &:last-child { - padding-bottom: 13px; - } - } - - .mx_IconizedContextMenu_optionList { - &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { - margin-top: 10px; - - li:first-child { - padding-top: 10px; - } - } - - li:first-child { - padding-top: 0; - } + .mx_IconizedContextMenu_optionList > * { + padding: 8px 16px 8px 11px; } } } diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index bbb1e1cc7b..c958b9eacd 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -86,6 +86,8 @@ limitations under the License. .mx_UserMenu_contextMenu_redRow { .mx_AccessibleButton { + padding-top: 16px; + padding-bottom: 16px; color: $warning-color !important; // !important to override styles from context menu } @@ -95,6 +97,8 @@ limitations under the License. } .mx_UserMenu_contextMenu_header { + padding: 20px; + // Create a flexbox to organize the header a bit easier display: flex; align-items: center; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index ef461bf1b4..bd222c7f25 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -191,12 +191,10 @@ export default class UserMenu extends React.Component { let homeButton = null; if (this.hasHomePage) { homeButton = ( -
  • - - - {_t("Home")} - -
  • + + + {_t("Home")} + ); } @@ -232,49 +230,33 @@ export default class UserMenu extends React.Component {
{hostingLink}
-
    - {homeButton} -
  • - this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - - {_t("Notification settings")} - -
  • -
  • - this.onSettingsOpen(e, USER_SECURITY_TAB)}> - - {_t("Security & privacy")} - -
  • -
  • - this.onSettingsOpen(e, null)}> - - {_t("All settings")} - -
  • -
  • - - - {_t("Archived rooms")} - -
  • -
  • - - - {_t("Feedback")} - -
  • -
+ {homeButton} + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> + + {_t("Notification settings")} + + this.onSettingsOpen(e, USER_SECURITY_TAB)}> + + {_t("Security & privacy")} + + this.onSettingsOpen(e, null)}> + + {_t("All settings")} + + + + {_t("Archived rooms")} + + + + {_t("Feedback")} +
-
-
    -
  • - - - {_t("Sign out")} - -
  • -
+
+ + + {_t("Sign out")} +
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 52b3d444ac..f290edc5dd 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -66,7 +66,7 @@ interface IState { generalMenuDisplayed: boolean; } -export const contextMenuBelow = (elementRect) => { +const contextMenuBelow = (elementRect) => { const left = elementRect.left + window.pageXOffset - 6; let top = elementRect.bottom + window.pageYOffset + 21; const chevronFace = "none"; @@ -185,32 +185,22 @@ export default class RoomTile2 extends React.Component {
-
    -
  • - - - {_t("All messages")} - -
  • -
  • - - - {_t("Default")} - -
  • -
  • - - - {_t("Mentions & Keywords")} - -
  • -
  • - - - {_t("None")} - -
  • -
+ + + {_t("All messages")} + + + + {_t("Default")} + + + + {_t("Mentions & Keywords")} + + + + {_t("None")} +
@@ -252,36 +242,20 @@ export default class RoomTile2 extends React.Component {
-
    -
  • - this.onTagRoom(e, DefaultTagID.Favourite)}> - - {_t("Favourite")} - -
  • -
  • - this.onTagRoom(e, DefaultTagID.LowPriority)}> - - {_t("Low Priority")} - -
  • -
  • - - - {_t("Settings")} - -
  • -
+ this.onTagRoom(e, DefaultTagID.Favourite)}> + + {_t("Favourite")} + + + + {_t("Settings")} +
-
-
    -
  • - - - {_t("Leave Room")} - -
  • -
+
+ + + {_t("Leave Room")} +
From 198958dcdd360baa20fe7459f5b0535edf7a3286 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 30 Jun 2020 22:32:59 +0100 Subject: [PATCH 06/35] Iterate to match figma Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.scss | 2 +- res/css/views/rooms/_RoomTile2.scss | 13 ++++ src/components/structures/UserMenu.tsx | 12 ++-- src/components/views/rooms/RoomTile2.tsx | 79 +++++++++++++++++------- src/i18n/strings/en_EN.json | 3 +- 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 0d057d96ab..880b01a10e 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -652,7 +652,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { max-width: 16px; } - span:last-child { // labels + span.mx_IconizedContextMenu_label { // labels padding-left: 14px; width: 100%; flex: 1; diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 78a7732882..8933c73045 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -166,6 +166,9 @@ limitations under the License. .mx_RoomTile2_iconBellCrossed::before { mask-image: url('$(res)/img/feather-customised/bell-crossed.svg'); } +.mx_RoomTile2_iconCheck::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); +} .mx_RoomTile2_contextMenu { .mx_RoomTile2_contextMenu_redRow { @@ -178,6 +181,16 @@ limitations under the License. } } + .mx_RoomTile2_contextMenu_activeRow { + &.mx_AccessibleButton, .mx_AccessibleButton { + color: $accent-color !important; // !important to override styles from context menu + } + + .mx_IconizedContextMenu_icon::before { + background-color: $accent-color; + } + } + .mx_IconizedContextMenu_icon { position: relative; width: 16px; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index bd222c7f25..4a45162902 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -233,29 +233,29 @@ export default class UserMenu extends React.Component { {homeButton} this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - {_t("Notification settings")} + {_t("Notification settings")} this.onSettingsOpen(e, USER_SECURITY_TAB)}> - {_t("Security & privacy")} + {_t("Security & privacy")} this.onSettingsOpen(e, null)}> - {_t("All settings")} + {_t("All settings")} - {_t("Archived rooms")} + {_t("Archived rooms")} - {_t("Feedback")} + {_t("Feedback")}
- {_t("Sign out")} + {_t("Sign out")}
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index f290edc5dd..4ed167d594 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -32,7 +32,7 @@ import NotificationBadge, { TagSpecificNotificationState } from "./NotificationBadge"; import { _t } from "../../../languageHandler"; -import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; +import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomTileIcon from "./RoomTileIcon"; @@ -73,6 +73,34 @@ const contextMenuBelow = (elementRect) => { return {left, top, chevronFace}; }; +type State = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE; + +interface INotifOptionProps { + active: boolean; + iconClassName: string; + label: string; + onClick(); +} + +const NotifOption: React.FC = ({active, onClick, iconClassName, label}) => { + const classes = classNames({ + mx_RoomTile2_contextMenu_activeRow: active, + }); + + let activeIcon; + if (active) { + activeIcon = ; + } + + return ( + + + { label } + { activeIcon } + + ); +}; + export default class RoomTile2 extends React.Component { private notificationsMenuButtonRef: React.RefObject = createRef(); private generalMenuButtonRef: React.RefObject = createRef(); @@ -178,6 +206,8 @@ export default class RoomTile2 extends React.Component { private renderNotificationsMenu(): React.ReactElement { if (this.props.isMinimized) return null; // no menu when minimized + const state = getRoomNotifsState(this.props.room.roomId); + let contextMenu = null; if (this.state.notificationsMenuDisplayed) { const elementRect = this.notificationsMenuButtonRef.current.getBoundingClientRect(); @@ -185,29 +215,36 @@ export default class RoomTile2 extends React.Component {
- - - {_t("All messages")} - - - - {_t("Default")} - - - - {_t("Mentions & Keywords")} - - - - {_t("None")} - + + + +
); } - const state = getRoomNotifsState(this.props.room.roomId); const classes = classNames("mx_RoomTile2_notificationsButton", { // Show bell icon for the default case too. mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES, @@ -244,17 +281,17 @@ export default class RoomTile2 extends React.Component {
this.onTagRoom(e, DefaultTagID.Favourite)}> - {_t("Favourite")} + {_t("Favourite")} - {_t("Settings")} + {_t("Settings")}
- {_t("Leave Room")} + {_t("Leave Room")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e056576cb1..b23264a297 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1218,11 +1218,11 @@ "%(count)s unread messages.|one": "1 unread message.", "Unread mentions.": "Unread mentions.", "Unread messages.": "Unread messages.", + "Use default": "Use default", "All messages": "All messages", "Mentions & Keywords": "Mentions & Keywords", "Notification options": "Notification options", "Favourite": "Favourite", - "Low Priority": "Low Priority", "Leave Room": "Leave Room", "Room options": "Room options", "Add a topic": "Add a topic", @@ -1903,6 +1903,7 @@ "Mentions only": "Mentions only", "Leave": "Leave", "Forget": "Forget", + "Low Priority": "Low Priority", "Direct Chat": "Direct Chat", "Clear status": "Clear status", "Update status": "Update status", From 508dea1c8976eb8c4e05ad5cdcd7f5a547b160f2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 30 Jun 2020 22:53:30 +0100 Subject: [PATCH 07/35] Wire up Notifications context menu on room tile 2 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 40 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 4ed167d594..aad9b00860 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -37,6 +37,8 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomTileIcon from "./RoomTileIcon"; import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { setRoomNotifsState } from "../../../RoomNotifs"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -73,13 +75,11 @@ const contextMenuBelow = (elementRect) => { return {left, top, chevronFace}; }; -type State = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE; - interface INotifOptionProps { active: boolean; iconClassName: string; label: string; - onClick(); + onClick(ev: ButtonEvent); } const NotifOption: React.FC = ({active, onClick, iconClassName, label}) => { @@ -203,8 +203,32 @@ export default class RoomTile2 extends React.Component { this.setState({generalMenuDisplayed: false}); // hide the menu }; + private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) { + ev.preventDefault(); + ev.stopPropagation(); + if (MatrixClientPeg.get().isGuest()) return; + + try { + // TODO add local echo + await setRoomNotifsState(this.props.room.roomId, newState); + } catch (error) { + // TODO: some form of error notification to the user to inform them that their state change failed. + console.error(error); + } + + // Close the context menu + this.setState({ + notificationsMenuDisplayed: false, + }); + } + + private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES); + private onClickAlertMe = ev => this.saveNotifState(ev, ALL_MESSAGES_LOUD); + private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY); + private onClickMute = ev => this.saveNotifState(ev, MUTE); + private renderNotificationsMenu(): React.ReactElement { - if (this.props.isMinimized) return null; // no menu when minimized + if (this.props.isMinimized || MatrixClientPeg.get().isGuest()) return null; // no menu when minimized or guest const state = getRoomNotifsState(this.props.room.roomId); @@ -219,25 +243,25 @@ export default class RoomTile2 extends React.Component { label={_t("Use default")} active={state === ALL_MESSAGES} iconClassName="mx_RoomTile2_iconBell" - onClick={this._onClickAllNotifs} + onClick={this.onClickAllNotifs} /> From dcd51b5be3369cf87a89d937c5eddb94669b2d58 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:24:46 +0100 Subject: [PATCH 08/35] Implement breadcrumb notifications and scrolling --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 3 +- .../views/avatars/_DecoratedRoomAvatar.scss | 33 ++++++++++ res/css/views/rooms/_RoomTile2.scss | 17 +---- src/components/structures/LeftPanel2.tsx | 2 +- .../views/avatars/DecoratedRoomAvatar.tsx | 65 +++++++++++++++++++ .../views/rooms/RoomBreadcrumbs2.tsx | 14 +++- src/components/views/rooms/RoomTile2.tsx | 24 ++++--- src/stores/room-list/RoomListStore2.ts | 18 ++++- src/stores/room-list/algorithms/Algorithm.ts | 11 ++++ 10 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 res/css/views/avatars/_DecoratedRoomAvatar.scss create mode 100644 src/components/views/avatars/DecoratedRoomAvatar.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index afc40ca0d6..8288cf34f6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -49,6 +49,7 @@ @import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_Welcome.scss"; @import "./views/avatars/_BaseAvatar.scss"; +@import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 40babaa9ca..bdaada0d15 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -70,7 +70,8 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_breadcrumbsContainer { width: 100%; - overflow: hidden; + overflow-y: hidden; + overflow-x: scroll; margin-top: 8px; } } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss new file mode 100644 index 0000000000..984fa0ce9a --- /dev/null +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -0,0 +1,33 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_DecoratedRoomAvatar { + position: relative; + + .mx_RoomTileIcon { + position: absolute; + bottom: 0; + right: 0; + } + + .mx_NotificationBadge { + position: absolute; + top: 0; + right: 0; + height: 18px; + width: 18px; + } +} diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 2845068de3..e4e6a3eac1 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -30,15 +30,8 @@ limitations under the License. border-radius: 32px; } - .mx_RoomTile2_avatarContainer { + .mx_DecoratedRoomAvatar { margin-right: 8px; - position: relative; - - .mx_RoomTileIcon { - position: absolute; - bottom: 0; - right: 0; - } } .mx_RoomTile2_nameContainer { @@ -145,16 +138,10 @@ limitations under the License. align-items: center; position: relative; - .mx_RoomTile2_avatarContainer { + .mx_DecoratedRoomAvatar { margin-right: 0; } - .mx_RoomTile2_badgeContainer { - position: absolute; - top: 0; - right: 0; - height: 18px; - } } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 0f614435e5..b4ec897561 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -151,7 +151,7 @@ export default class LeftPanel2 extends React.Component { let breadcrumbs; if (this.state.showBreadcrumbs) { breadcrumbs = ( -
+
{this.props.isMinimized ? null : }
); diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx new file mode 100644 index 0000000000..af1cdc779c --- /dev/null +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -0,0 +1,65 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { TagID } from '../../../stores/room-list/models'; +import RoomAvatar from "./RoomAvatar"; +import RoomTileIcon from "../rooms/RoomTileIcon"; +import NotificationBadge, { INotificationState, TagSpecificNotificationState } from '../rooms/NotificationBadge'; + +interface IProps { + room: Room; + avatarSize: number; + tag: TagID; + displayBadge?: boolean; + forceCount?: boolean; +} + +interface IState { + notificationState?: INotificationState; +} + +export default class DecoratedRoomAvatar extends React.PureComponent { + + constructor(props: IProps) { + super(props); + + this.state = { + notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), + } + } + + public render(): React.ReactNode { + console.log({tag: this.props.tag}) + + let badge: React.ReactNode; + if (this.props.displayBadge) { + badge = ; + } + + return
+ + + {badge} +
+ } +} \ No newline at end of file diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index bd12ced6ee..2f2b815002 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -17,13 +17,15 @@ limitations under the License. import React from "react"; import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; import AccessibleButton from "../elements/AccessibleButton"; -import RoomAvatar from "../avatars/RoomAvatar"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import { _t } from "../../../languageHandler"; import { Room } from "matrix-js-sdk/src/models/room"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import Analytics from "../../../Analytics"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { CSSTransition } from "react-transition-group"; +import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import { DefaultTagID } from "../../../stores/room-list/models"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -93,6 +95,8 @@ export default class RoomBreadcrumbs2 extends React.PureComponent { + const roomTags = RoomListStore.instance.getTagsForRoom(r) + const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; return ( this.viewRoom(r, i)} aria-label={_t("Room %(name)s", {name: r.name})} > - + ); }); diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 1284728855..f8c46ee85a 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -22,7 +22,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; -import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; @@ -35,7 +34,7 @@ import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; -import RoomTileIcon from "./RoomTileIcon"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -233,13 +232,22 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ( - + + let badge: React.ReactNode; + if (!this.props.isMinimized) { + badge = - ); + } // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; @@ -277,7 +285,6 @@ export default class RoomTile2 extends React.Component { ); if (this.props.isMinimized) nameContainer = null; - const avatarSize = 32; return ( @@ -292,10 +299,7 @@ export default class RoomTile2 extends React.Component { onClick={this.onTileClick} role="treeitem" > -
- - -
+ {roomAvatar} {nameContainer}
{badge} diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 497b8e5530..b4d96becc4 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -17,7 +17,7 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; -import { OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; +import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import TagOrderStore from "../TagOrderStore"; import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -187,7 +187,8 @@ export class RoomListStore2 extends AsyncStore { const room = this.matrixClient.getRoom(roomId); const tryUpdate = async (updatedRoom: Room) => { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 - console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`); + console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` + + ` in ${updatedRoom.roomId}`); if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`); @@ -422,6 +423,19 @@ export class RoomListStore2 extends AsyncStore { } } } + + /** + * Gets the tags for a room identified by the store. The returned set + * should never be empty, and will contain DefaultTagID.Untagged if + * the store is not aware of any tags. + * @param room The room to get the tags for. + * @returns The tags for the room. + */ + public getTagsForRoom(room: Room): TagID[] { + const algorithmTags = this.algorithm.getTagsForRoom(room); + if (!algorithmTags) return [DefaultTagID.Untagged]; + return algorithmTags; + } } export default class RoomListStore { diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 8215d2ef57..d4615356da 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -670,4 +670,15 @@ export class Algorithm extends EventEmitter { return true; } + + /** + * Returns the tags for a given room as known by the algorithm. May be null or + * empty. + * @param room The room to get known tags for. + * @returns The known tags for the room. + */ + public getTagsForRoom(room: Room): TagID[] { + if (!room) throw new Error("A room is required"); + return this.roomIdsToTags[room.roomId]; + } } From 0904ae8c7a27c6417f355a68010399754f48e350 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:35:59 +0100 Subject: [PATCH 09/35] Bug fixes --- .../views/avatars/DecoratedRoomAvatar.tsx | 2 +- src/stores/room-list/algorithms/Algorithm.ts | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index af1cdc779c..5fb3287980 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -58,7 +58,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent - + {badge}
} diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index d4615356da..36abf86975 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -524,7 +524,7 @@ export class Algorithm extends EventEmitter { } } - private getTagsForRoom(room: Room): TagID[] { + public getTagsForRoom(room: Room): TagID[] { // XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly // different use case and therefore different performance curve @@ -670,15 +670,4 @@ export class Algorithm extends EventEmitter { return true; } - - /** - * Returns the tags for a given room as known by the algorithm. May be null or - * empty. - * @param room The room to get known tags for. - * @returns The known tags for the room. - */ - public getTagsForRoom(room: Room): TagID[] { - if (!room) throw new Error("A room is required"); - return this.roomIdsToTags[room.roomId]; - } } From 2379ec577cbfa6e3e83ab5f06b339a5ef36ed06f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:39:25 +0100 Subject: [PATCH 10/35] Lint semicolons --- src/components/views/avatars/DecoratedRoomAvatar.tsx | 6 ++---- src/components/views/rooms/RoomBreadcrumbs2.tsx | 2 +- src/components/views/rooms/RoomTile2.tsx | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 5fb3287980..1156c80313 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -41,12 +41,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent {badge} -
+ ; } } \ No newline at end of file diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 2f2b815002..c0417fc592 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -95,7 +95,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent { - const roomTags = RoomListStore.instance.getTagsForRoom(r) + const roomTags = RoomListStore.instance.getTagsForRoom(r); const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; return ( { avatarSize={avatarSize} tag={this.props.tag} displayBadge={this.props.isMinimized} - /> + />; - let badge: React.ReactNode; + let badge: React.ReactNode; if (!this.props.isMinimized) { badge = + />; } // TODO: the original RoomTile uses state for the room name. Do we need to? From b1e0b35758cd28dd7e967c48dc181b5c7af12aeb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:40:24 +0100 Subject: [PATCH 11/35] Lint style --- res/css/views/rooms/_RoomTile2.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index e4e6a3eac1..0a425d890f 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -141,7 +141,6 @@ limitations under the License. .mx_DecoratedRoomAvatar { margin-right: 0; } - } } From d2fb30a2116313c70943eb0e7bc37e644c2a7e7f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:52:13 +0100 Subject: [PATCH 12/35] Hide scrollbar without pixel jumping --- src/components/structures/LeftPanel2.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 0f614435e5..d34986f981 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import * as React from "react"; +import classnames from 'classnames'; import { createRef } from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; @@ -205,6 +206,11 @@ export default class LeftPanel2 extends React.Component { "mx_LeftPanel2_minimized": this.props.isMinimized, }); + const className = classnames( + "mx_LeftPanel2_actualRoomListContainer", + "mx_AutoHideScrollbar", + ); + return (
{tagPanel} @@ -212,7 +218,7 @@ export default class LeftPanel2 extends React.Component { {this.renderHeader()} {this.renderSearchExplore()}
{roomList}
From 1dd9c1eea3b420fba4dc34e034e265c90a6af9bf Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 12:28:32 +0100 Subject: [PATCH 13/35] Use avatar sisze inplace --- src/components/views/rooms/RoomTile2.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c36f504409..ed0044bacb 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -232,10 +232,9 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const avatarSize = 32; const roomAvatar = ; From b5014282a4ed02ca8c186d95a0093f75bba07e11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 13:59:50 +0100 Subject: [PATCH 14/35] Iterate PR based on Figma design and feedback Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.scss | 1 + res/css/views/rooms/_RoomTile2.scss | 5 +++++ src/components/structures/UserMenu.tsx | 1 + src/components/views/rooms/RoomTile2.tsx | 7 ++++--- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 880b01a10e..6e70618142 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -631,6 +631,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_AccessibleButton { + // pad the inside of the button so that the hover background is padded too padding-top: 12px; padding-bottom: 12px; text-decoration: none; diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 8933c73045..44c5b6ee17 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -92,6 +92,7 @@ limitations under the License. justify-content: center; } + // The context menu buttons are hidden by default .mx_RoomTile2_menuButton, .mx_RoomTile2_notificationsButton { width: 20px; @@ -114,6 +115,7 @@ limitations under the License. } } + // If the room has an overriden notification setting then we always show the notifications menu button .mx_RoomTile2_notificationsButton.mx_RoomTile2_notificationsButton_show { display: block; } @@ -166,6 +168,9 @@ limitations under the License. .mx_RoomTile2_iconBellCrossed::before { mask-image: url('$(res)/img/feather-customised/bell-crossed.svg'); } +.mx_RoomTile2_iconBellMentions::before { + mask-image: url('$(res)/img/feather-customised/bell-mentions.custom.svg'); +} .mx_RoomTile2_iconCheck::before { mask-image: url('$(res)/img/feather-customised/check.svg'); } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 4a45162902..d6771f3011 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -202,6 +202,7 @@ export default class UserMenu extends React.Component { return ( { const left = elementRect.left + window.pageXOffset - 6; - let top = elementRect.bottom + window.pageYOffset + 21; + let top = elementRect.bottom + window.pageYOffset + 17; const chevronFace = "none"; return {left, top, chevronFace}; }; @@ -209,10 +209,11 @@ export default class RoomTile2 extends React.Component { if (MatrixClientPeg.get().isGuest()) return; try { - // TODO add local echo + // TODO add local echo - https://github.com/vector-im/riot-web/issues/14280 await setRoomNotifsState(this.props.room.roomId, newState); } catch (error) { // TODO: some form of error notification to the user to inform them that their state change failed. + // https://github.com/vector-im/riot-web/issues/14281 console.error(error); } @@ -254,7 +255,7 @@ export default class RoomTile2 extends React.Component { Date: Wed, 1 Jul 2020 14:05:33 +0100 Subject: [PATCH 15/35] align context menus even better Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index d518c74aa7..e12be8b30e 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -69,7 +69,8 @@ interface IState { } const contextMenuBelow = (elementRect) => { - const left = elementRect.left + window.pageXOffset - 6; + // align the context menu's icons with the icon which opened the context menu + const left = elementRect.left + window.pageXOffset - 9; let top = elementRect.bottom + window.pageYOffset + 17; const chevronFace = "none"; return {left, top, chevronFace}; From 0cbc506ad615aaa5bd37e732edf9712021c9a175 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 14:26:50 +0100 Subject: [PATCH 16/35] add missing svg Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/img/feather-customised/bell-mentions.custom.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 res/img/feather-customised/bell-mentions.custom.svg diff --git a/res/img/feather-customised/bell-mentions.custom.svg b/res/img/feather-customised/bell-mentions.custom.svg new file mode 100644 index 0000000000..fcc02f337f --- /dev/null +++ b/res/img/feather-customised/bell-mentions.custom.svg @@ -0,0 +1,3 @@ + + + From 9831698b1e004f7166e64c83a06583a1a162b9a8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 14:28:00 +0100 Subject: [PATCH 17/35] Hide notifications menu from invite tiles Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 882bd21084..c9a1f39982 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -230,7 +230,10 @@ export default class RoomTile2 extends React.Component { private onClickMute = ev => this.saveNotifState(ev, MUTE); private renderNotificationsMenu(): React.ReactElement { - if (this.props.isMinimized || MatrixClientPeg.get().isGuest()) return null; // no menu when minimized or guest + if (this.props.isMinimized || MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Invite) { + // the menu makes no sense in these cases so do not show one + return null; + } const state = getRoomNotifsState(this.props.room.roomId); From ad27dbbfab6e7d2c3cdc07fc48bbad237a9b0465 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 15:15:18 +0100 Subject: [PATCH 18/35] Clean up classnames --- src/components/structures/LeftPanel2.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index d34986f981..ab117d55ed 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import * as React from "react"; -import classnames from 'classnames'; import { createRef } from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; @@ -206,7 +205,7 @@ export default class LeftPanel2 extends React.Component { "mx_LeftPanel2_minimized": this.props.isMinimized, }); - const className = classnames( + const roomListClasses = classNames( "mx_LeftPanel2_actualRoomListContainer", "mx_AutoHideScrollbar", ); @@ -218,7 +217,7 @@ export default class LeftPanel2 extends React.Component { {this.renderHeader()} {this.renderSearchExplore()}
{roomList}
From 2162517a37cae3f5ea6a422d14c5d2f1429b6939 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 16:05:27 +0100 Subject: [PATCH 19/35] Display breadcrumbs only after 20 rooms have been joined --- src/components/structures/LeftPanel2.tsx | 3 +++ src/stores/BreadcrumbsStore.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index b4ec897561..fb07c9c601 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -30,6 +30,7 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; import SettingsStore from "../../settings/SettingsStore"; +import RoomListStore, { RoomListStore2, LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -69,6 +70,7 @@ export default class LeftPanel2 extends React.Component { }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); @@ -81,6 +83,7 @@ export default class LeftPanel2 extends React.Component { public componentWillUnmount() { SettingsStore.unwatchSetting(this.tagPanelWatcherRef); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 1c47075cbb..43bef8a538 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -21,6 +21,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; +import _reduce from 'lodash/reduce'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -51,7 +52,10 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } public get visible(): boolean { - return this.state.enabled; + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0) + console.log(`calculating roomlist size: ${roomCount}`) + return roomCount >= 20; } protected async onAction(payload: ActionPayload) { From d203943b7fd9b7877767db3de3684a6e671a2d84 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 16:07:27 +0100 Subject: [PATCH 20/35] lint semis --- src/stores/BreadcrumbsStore.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 43bef8a538..9905dd4345 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -53,8 +53,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { public get visible(): boolean { // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0) - console.log(`calculating roomlist size: ${roomCount}`) + const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0); return roomCount >= 20; } From 89bd572371e695e861b6d76c23943c1193ff65da Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:05:37 +0100 Subject: [PATCH 21/35] Fix context menu nesting causing bubbling and instabilities Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index 98b0867ccc..a56a987fcf 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -116,6 +116,7 @@ export class ContextMenu extends React.Component { this.props.onFinished(); e.preventDefault(); + e.stopPropagation(); const x = e.clientX; const y = e.clientY; @@ -133,6 +134,12 @@ export class ContextMenu extends React.Component { } }; + onContextMenuPreventBubbling = (e) => { + // stop propagation so that any context menu handlers don't leak out of this context menu + // but do not inhibit the default browser menu + e.stopPropagation(); + }; + _onMoveFocus = (element, up) => { let descending = false; // are we currently descending or ascending through the DOM tree? @@ -324,7 +331,7 @@ export class ContextMenu extends React.Component { } return ( -
+
{ chevron } { props.children } From 5c2b291510aa2cbacd7785b0d9a37eca0844121f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:06:26 +0100 Subject: [PATCH 22/35] Support right click context menu interactions on Room List 2 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 40 ++++++++---- src/components/views/rooms/RoomSublist2.tsx | 42 ++++++++----- src/components/views/rooms/RoomTile2.tsx | 70 +++++++++++---------- 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index d6771f3011..92a4666a9d 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -42,8 +42,10 @@ interface IProps { isMinimized: boolean; } +type PartialDOMRect = Pick; + interface IState { - menuDisplayed: boolean; + contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; } @@ -56,7 +58,7 @@ export default class UserMenu extends React.Component { super(props); this.state = { - menuDisplayed: false, + contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), }; @@ -106,13 +108,27 @@ export default class UserMenu extends React.Component { private onOpenMenuClick = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); - this.setState({menuDisplayed: true}); + const target = ev.target as HTMLButtonElement; + this.setState({contextMenuPosition: target.getBoundingClientRect()}); + }; + + private onContextMenu = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ + contextMenuPosition: { + left: ev.clientX, + top: ev.clientY, + width: 20, + height: 0, + }, + }); }; private onCloseMenu = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); - this.setState({menuDisplayed: false}); + this.setState({contextMenuPosition: null}); }; private onSwitchThemeClick = () => { @@ -129,7 +145,7 @@ export default class UserMenu extends React.Component { const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; defaultDispatcher.dispatch(payload); - this.setState({menuDisplayed: false}); // also close the menu + this.setState({contextMenuPosition: null}); // also close the menu }; private onShowArchived = (ev: ButtonEvent) => { @@ -145,7 +161,7 @@ export default class UserMenu extends React.Component { ev.stopPropagation(); Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); - this.setState({menuDisplayed: false}); // also close the menu + this.setState({contextMenuPosition: null}); // also close the menu }; private onSignOutClick = (ev: ButtonEvent) => { @@ -153,7 +169,7 @@ export default class UserMenu extends React.Component { ev.stopPropagation(); Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); - this.setState({menuDisplayed: false}); // also close the menu + this.setState({contextMenuPosition: null}); // also close the menu }; private onHomeClick = (ev: ButtonEvent) => { @@ -164,7 +180,7 @@ export default class UserMenu extends React.Component { }; private renderContextMenu = (): React.ReactNode => { - if (!this.state.menuDisplayed) return null; + if (!this.state.contextMenuPosition) return null; let hostingLink; const signupLink = getHostingLink("user-context-menu"); @@ -198,13 +214,12 @@ export default class UserMenu extends React.Component { ); } - const elementRect = this.buttonRef.current.getBoundingClientRect(); return (
@@ -290,7 +305,8 @@ export default class UserMenu extends React.Component { onClick={this.onOpenMenuClick} inputRef={this.buttonRef} label={_t("Account settings")} - isExpanded={this.state.menuDisplayed} + isExpanded={!!this.state.contextMenuPosition} + onContextMenu={this.onContextMenu} >
diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 461098cb63..36d38c7087 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -65,22 +65,21 @@ interface IProps { // TODO: Account for https://github.com/vector-im/riot-web/issues/14179 } +type PartialDOMRect = Pick; + interface IState { notificationState: ListNotificationState; - menuDisplayed: boolean; + contextMenuPosition: PartialDOMRect; isResizing: boolean; } export default class RoomSublist2 extends React.Component { - private headerButton = createRef(); - private menuButtonRef: React.RefObject = createRef(); - constructor(props: IProps) { super(props); this.state = { notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId), - menuDisplayed: false, + contextMenuPosition: null, isResizing: false, }; this.state.notificationState.setRooms(this.props.rooms); @@ -132,11 +131,24 @@ export default class RoomSublist2 extends React.Component { private onOpenMenuClick = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); - this.setState({menuDisplayed: true}); + const target = ev.target as HTMLButtonElement; + this.setState({contextMenuPosition: target.getBoundingClientRect()}); + }; + + private onContextMenu = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ + contextMenuPosition: { + left: ev.clientX, + top: ev.clientY, + height: 0, + }, + }); }; private onCloseMenu = () => { - this.setState({menuDisplayed: false}); + this.setState({contextMenuPosition: null}); }; private onUnreadFirstChanged = async () => { @@ -202,15 +214,14 @@ export default class RoomSublist2 extends React.Component { } let contextMenu = null; - if (this.state.menuDisplayed) { - const elementRect = this.menuButtonRef.current.getBoundingClientRect(); + if (this.state.contextMenuPosition) { const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; contextMenu = (
@@ -261,9 +272,8 @@ export default class RoomSublist2 extends React.Component { {contextMenu} @@ -272,7 +282,7 @@ export default class RoomSublist2 extends React.Component { private renderHeader(): React.ReactElement { return ( - + {({onFocus, isActive, ref}) => { // TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180 const tabIndex = isActive ? 0 : -1; @@ -317,12 +327,14 @@ export default class RoomSublist2 extends React.Component {
{this.props.label} @@ -347,7 +359,7 @@ export default class RoomSublist2 extends React.Component { const classes = classNames({ 'mx_RoomSublist2': true, - 'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed, + 'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition, 'mx_RoomSublist2_minimized': this.props.isMinimized, }); diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c9a1f39982..47b5b4206b 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -60,15 +60,17 @@ interface IProps { // TODO: Incoming call boxes: https://github.com/vector-im/riot-web/issues/14177 } +type PartialDOMRect = Pick; + interface IState { hover: boolean; notificationState: INotificationState; selected: boolean; - notificationsMenuDisplayed: boolean; - generalMenuDisplayed: boolean; + notificationsMenuPosition: PartialDOMRect; + generalMenuPosition: PartialDOMRect; } -const contextMenuBelow = (elementRect) => { +const contextMenuBelow = (elementRect: PartialDOMRect) => { // align the context menu's icons with the icon which opened the context menu const left = elementRect.left + window.pageXOffset - 9; let top = elementRect.bottom + window.pageYOffset + 17; @@ -103,9 +105,6 @@ const NotifOption: React.FC = ({active, onClick, iconClassNam }; export default class RoomTile2 extends React.Component { - private notificationsMenuButtonRef: React.RefObject = createRef(); - private generalMenuButtonRef: React.RefObject = createRef(); - // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180 constructor(props: IProps) { @@ -115,8 +114,8 @@ export default class RoomTile2 extends React.Component { hover: false, notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, - notificationsMenuDisplayed: false, - generalMenuDisplayed: false, + notificationsMenuPosition: null, + generalMenuPosition: null, }; ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); @@ -137,6 +136,8 @@ export default class RoomTile2 extends React.Component { }; private onTileClick = (ev: React.KeyboardEvent) => { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'view_room', // TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233 @@ -153,25 +154,34 @@ export default class RoomTile2 extends React.Component { private onNotificationsMenuOpenClick = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); - this.setState({notificationsMenuDisplayed: true}); + const target = ev.target as HTMLButtonElement; + this.setState({notificationsMenuPosition: target.getBoundingClientRect()}); }; - private onCloseNotificationsMenu = (ev: InputEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({notificationsMenuDisplayed: false}); + private onCloseNotificationsMenu = () => { + this.setState({notificationsMenuPosition: null}); }; private onGeneralMenuOpenClick = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); - this.setState({generalMenuDisplayed: true}); + const target = ev.target as HTMLButtonElement; + this.setState({generalMenuPosition: target.getBoundingClientRect()}); }; - private onCloseGeneralMenu = (ev: InputEvent) => { + private onContextMenu = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); - this.setState({generalMenuDisplayed: false}); + this.setState({ + generalMenuPosition: { + left: ev.clientX, + bottom: ev.clientY, + }, + }); + }; + + private onCloseGeneralMenu = () => { + this.setState({generalMenuPosition: null}); }; private onTagRoom = (ev: ButtonEvent, tagId: TagID) => { @@ -190,7 +200,7 @@ export default class RoomTile2 extends React.Component { action: 'leave_room', room_id: this.props.room.roomId, }); - this.setState({generalMenuDisplayed: false}); // hide the menu + this.setState({generalMenuPosition: null}); // hide the menu }; private onOpenRoomSettings = (ev: ButtonEvent) => { @@ -201,7 +211,7 @@ export default class RoomTile2 extends React.Component { action: 'open_room_settings', room_id: this.props.room.roomId, }); - this.setState({generalMenuDisplayed: false}); // hide the menu + this.setState({generalMenuPosition: null}); // hide the menu }; private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) { @@ -218,10 +228,7 @@ export default class RoomTile2 extends React.Component { console.error(error); } - // Close the context menu - this.setState({ - notificationsMenuDisplayed: false, - }); + this.setState({notificationsMenuPosition: null}); // Close the context menu } private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES); @@ -238,10 +245,9 @@ export default class RoomTile2 extends React.Component { const state = getRoomNotifsState(this.props.room.roomId); let contextMenu = null; - if (this.state.notificationsMenuDisplayed) { - const elementRect = this.notificationsMenuButtonRef.current.getBoundingClientRect(); + if (this.state.notificationsMenuPosition) { contextMenu = ( - +
{ {contextMenu} @@ -307,10 +312,9 @@ export default class RoomTile2 extends React.Component { } let contextMenu = null; - if (this.state.generalMenuDisplayed) { - const elementRect = this.generalMenuButtonRef.current.getBoundingClientRect(); + if (this.state.generalMenuPosition) { contextMenu = ( - +
this.onTagRoom(e, DefaultTagID.Favourite)}> @@ -338,9 +342,8 @@ export default class RoomTile2 extends React.Component { {contextMenu} @@ -354,7 +357,7 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile2': true, 'mx_RoomTile2_selected': this.state.selected, - 'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed || this.state.notificationsMenuDisplayed, + 'mx_RoomTile2_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition), 'mx_RoomTile2_minimized': this.props.isMinimized, }); @@ -416,6 +419,7 @@ export default class RoomTile2 extends React.Component { onMouseLeave={this.onTileMouseLeave} onClick={this.onTileClick} role="treeitem" + onContextMenu={this.onContextMenu} >
From e8702aafa564030699e81a511aaeebcdd2785df4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:09:02 +0100 Subject: [PATCH 23/35] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 47b5b4206b..7a07027913 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -73,7 +73,7 @@ interface IState { const contextMenuBelow = (elementRect: PartialDOMRect) => { // align the context menu's icons with the icon which opened the context menu const left = elementRect.left + window.pageXOffset - 9; - let top = elementRect.bottom + window.pageYOffset + 17; + const top = elementRect.bottom + window.pageYOffset + 17; const chevronFace = "none"; return {left, top, chevronFace}; }; From 6424ffb22a6e99ad61a094c4f8433b42c8d9f93b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:13:54 +0100 Subject: [PATCH 24/35] fix repeated context menu interaction by not erroring Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 92a4666a9d..1cfe244845 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -125,9 +125,7 @@ export default class UserMenu extends React.Component { }); }; - private onCloseMenu = (ev: InputEvent) => { - ev.preventDefault(); - ev.stopPropagation(); + private onCloseMenu = () => { this.setState({contextMenuPosition: null}); }; From 4b27a67e336854f1f634b582ee3670a16a931623 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Jul 2020 23:16:54 +0100 Subject: [PATCH 25/35] improve default behaviour for consistency Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index a56a987fcf..5ba2662796 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -347,10 +347,18 @@ export class ContextMenu extends React.Component { } // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => { +export const ContextMenuButton = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return ( - + { children } ); From 992349944a911a686e2992aecf4ea0df772d5a42 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 12:18:48 +0100 Subject: [PATCH 26/35] Fix room list 2's room tile wrapping wrongly --- res/css/views/rooms/_RoomTile2.scss | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 44c5b6ee17..144a5ccf86 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -23,7 +23,6 @@ limitations under the License. // The tile is also a flexbox row itself display: flex; - flex-wrap: wrap; &.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen { background-color: $roomtile2-selected-bg-color; @@ -43,7 +42,8 @@ limitations under the License. .mx_RoomTile2_nameContainer { flex-grow: 1; - max-width: calc(100% - 58px); // 32px avatar, 18px badge area, 8px margin on avatar + min-width: 0; // allow flex to shrink it + margin-right: 8px; // spacing to buttons/badges // Create a new column layout flexbox for the name parts display: flex; @@ -81,8 +81,20 @@ limitations under the License. } } + //.mx_RoomTile2_badgeContainer, + .mx_RoomTile2_menuButton, + .mx_RoomTile2_notificationsButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin: auto 0; + } + + .mx_RoomTile2_menuButton { + margin-left: 4px; // spacing between buttons + } + .mx_RoomTile2_badgeContainer { - width: 18px; height: 32px; // Create another flexbox row because it's super easy to position the badge at @@ -90,14 +102,15 @@ limitations under the License. display: flex; align-items: center; justify-content: center; + + .mx_NotificationBadge { + margin-right: 2px; + } } // The context menu buttons are hidden by default .mx_RoomTile2_menuButton, .mx_RoomTile2_notificationsButton { - width: 20px; - height: 20px; - margin: auto 0 auto 8px; position: relative; display: none; @@ -130,7 +143,7 @@ limitations under the License. .mx_RoomTile2_badgeContainer { width: 0; height: 0; - visibility: hidden; + display: none; } .mx_RoomTile2_notificationsButton, From c259408d71faab9b788174572b5a7bbf5f6e4d61 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 12:35:06 +0100 Subject: [PATCH 27/35] fix alignment of dot and simplify CSS rules Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomTile2.scss | 31 +++++++++++++---------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 144a5ccf86..6241b3d0ba 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -81,36 +81,33 @@ limitations under the License. } } - //.mx_RoomTile2_badgeContainer, - .mx_RoomTile2_menuButton, - .mx_RoomTile2_notificationsButton { - width: 20px; - min-width: 20px; // yay flex - height: 20px; - margin: auto 0; - } - .mx_RoomTile2_menuButton { margin-left: 4px; // spacing between buttons } .mx_RoomTile2_badgeContainer { - height: 32px; - - // Create another flexbox row because it's super easy to position the badge at - // the end this way. - display: flex; - align-items: center; - justify-content: center; + height: 16px; + // don't set width so that it takes no space when there is no badge to show + margin: auto 0; // vertically align .mx_NotificationBadge { - margin-right: 2px; + margin-right: 2px; // centering + } + + .mx_NotificationBadge_dot { + // make the smaller dot occupy the same width for centering + margin-left: 5px; + margin-right: 7px; } } // The context menu buttons are hidden by default .mx_RoomTile2_menuButton, .mx_RoomTile2_notificationsButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin: auto 0; position: relative; display: none; From b5c94acbe6d5541c3fd94c7b1622a48f3e22bbe6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 2 Jul 2020 13:17:51 +0100 Subject: [PATCH 28/35] Remove unused crypto import --- src/components/structures/MatrixChat.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 79bdf743ce..9e3e112a28 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -23,7 +23,6 @@ import * as Matrix from "matrix-js-sdk"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; // what-input helps improve keyboard accessibility From a928785f723356c839ce00d4974a654825945d7c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 2 Jul 2020 13:19:27 +0100 Subject: [PATCH 29/35] Check whether crypto is enabled in room recovery reminder This avoids a soft crash that may occur otherwise. Fixes https://github.com/vector-im/riot-web/issues/14289 --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 19f1cccebd..519c4c1f8e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1819,6 +1819,7 @@ export default createReactClass({ ); const showRoomRecoveryReminder = ( + this.context.isCryptoEnabled() && SettingsStore.getValue("showRoomRecoveryReminder") && this.context.isRoomEncrypted(this.state.room.roomId) && this.context.getKeyBackupEnabled() === false From 1c0d46b6e1a15caa5efd5084583cf2715c11055f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 2 Jul 2020 15:26:51 +0100 Subject: [PATCH 30/35] Make breadcrumbs respsect setting --- src/stores/BreadcrumbsStore.ts | 5 +---- src/stores/room-list/RoomListStore2.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 9905dd4345..c78f15c3b4 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -21,7 +21,6 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; -import _reduce from 'lodash/reduce'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -52,9 +51,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } public get visible(): boolean { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0); - return roomCount >= 20; + return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20; } protected async onAction(payload: ActionPayload) { diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index b4d96becc4..73256e4de4 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -188,7 +188,7 @@ export class RoomListStore2 extends AsyncStore { const tryUpdate = async (updatedRoom: Room) => { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` + - ` in ${updatedRoom.roomId}`); + ` in ${updatedRoom.roomId}`); if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`); From b7aa8203b683d51ceeb1017cc435955bf9679ddd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 09:04:38 -0600 Subject: [PATCH 31/35] Wedge community invites into the new room list Fixes https://github.com/vector-im/riot-web/issues/14179 Disclaimer: this is all of the horrible because it's not meant to be here. Invites in general are likely to move out of the room list, which means this is temporary. Additionally, the communities rework will take care of this more correctly. For now, we support the absolute bare minimum to have them shown. --- src/components/views/rooms/RoomList2.tsx | 40 ++++++ src/components/views/rooms/RoomSublist2.tsx | 19 ++- src/components/views/rooms/TemporaryTile.tsx | 114 ++++++++++++++++++ .../room-list/filters/NameFilterCondition.ts | 8 +- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/components/views/rooms/TemporaryTile.tsx diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index a1298e107b..606f2d60e9 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -25,10 +25,15 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import dis from "../../../dispatcher/dispatcher"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; import { ActionPayload } from "../../../dispatcher/payloads"; import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; import { ListLayout } from "../../../stores/room-list/ListLayout"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import GroupAvatar from "../avatars/GroupAvatar"; +import TemporaryTile from "./TemporaryTile"; +import { NotificationColor, StaticNotificationState } from "./NotificationBadge"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -173,6 +178,39 @@ export default class RoomList2 extends React.Component { }); } + private renderCommunityInvites(): React.ReactElement[] { + // TODO: Put community invites in a more sensible place (not in the room list) + return MatrixClientPeg.get().getGroups().filter(g => { + if (g.myMembership !== 'invite') return false; + return !this.searchFilter || this.searchFilter.matches(g.name); + }).map(g => { + const avatar = ( + + ); + const openGroup = () => { + defaultDispatcher.dispatch({ + action: 'view_group', + group_id: g.groupId, + }); + }; + return ( + + ); + }); + } + private renderSublists(): React.ReactElement[] { const components: React.ReactElement[] = []; @@ -195,6 +233,7 @@ export default class RoomList2 extends React.Component { if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; + const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null; components.push( { isInvite={aesthetics.isInvite} layout={this.state.layouts.get(orderedTagId)} isMinimized={this.props.isMinimized} + extraBadTilesThatShouldntExist={extraTiles} /> ); } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c64d62ebea..87796924e8 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -62,6 +62,10 @@ interface IProps { isMinimized: boolean; tagId: TagID; + // TODO: Don't use this. It's for community invites, and community invites shouldn't be here. + // You should feel bad if you use this. + extraBadTilesThatShouldntExist?: React.ReactElement[]; + // TODO: Account for https://github.com/vector-im/riot-web/issues/14179 } @@ -87,8 +91,7 @@ export default class RoomSublist2 extends React.Component { } private get numTiles(): number { - // TODO: Account for group invites: https://github.com/vector-im/riot-web/issues/14179 - return (this.props.rooms || []).length; + return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length; } private get numVisibleTiles(): number { @@ -187,6 +190,10 @@ export default class RoomSublist2 extends React.Component { const tiles: React.ReactElement[] = []; + if (this.props.extraBadTilesThatShouldntExist) { + tiles.push(...this.props.extraBadTilesThatShouldntExist); + } + if (this.props.rooms) { const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles); for (const room of visibleRooms) { @@ -202,6 +209,14 @@ export default class RoomSublist2 extends React.Component { } } + // We only have to do this because of the extra tiles. We do it conditionally + // to avoid spending cycles on slicing. It's generally fine to do this though + // as users are unlikely to have more than a handful of tiles when the extra + // tiles are used. + if (tiles.length > this.numVisibleTiles) { + return tiles.slice(0, this.numVisibleTiles); + } + return tiles; } diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx new file mode 100644 index 0000000000..676969cade --- /dev/null +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -0,0 +1,114 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import classNames from "classnames"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import NotificationBadge, { INotificationState, NotificationColor } from "./NotificationBadge"; + +interface IProps { + isMinimized: boolean; + isSelected: boolean; + displayName: string; + avatar: React.ReactElement; + notificationState: INotificationState; + onClick: () => void; +} + +interface IState { + hover: boolean; +} + +export default class TemporaryTile extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + hover: false, + }; + } + + private onTileMouseEnter = () => { + this.setState({hover: true}); + }; + + private onTileMouseLeave = () => { + this.setState({hover: false}); + }; + + public render(): React.ReactElement { + // XXX: We copy classes because it's easier + const classes = classNames({ + 'mx_RoomTile2': true, + 'mx_RoomTile2_selected': this.props.isSelected, + 'mx_RoomTile2_minimized': this.props.isMinimized, + }); + + const badge = ( + + ); + + let name = this.props.displayName; + if (typeof name !== 'string') name = ''; + name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon + + const nameClasses = classNames({ + "mx_RoomTile2_name": true, + "mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold, + }); + + let nameContainer = ( +
+
+ {name} +
+
+ ); + if (this.props.isMinimized) nameContainer = null; + + const avatarSize = 32; + return ( + + + {({onFocus, isActive, ref}) => + +
+ {this.props.avatar} +
+ {nameContainer} +
+ {badge} +
+
+ } +
+
+ ); + } +} diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 8625cd932c..12f147990d 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -60,11 +60,15 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name + return this.matches(room.name); + } + + public matches(val: string): boolean { // Note: we have to match the filter with the removeHiddenChars() room name because the // function strips spaces and other characters (M becomes RN for example, in lowercase). // We also doubly convert to lowercase to work around oddities of the library. - const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase(); - const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase(); + const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase(); + const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase(); return noSecretsName.includes(noSecretsFilter); } } From 32642d592c9599f54853d3f850fb021eac3c40f2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 09:27:42 -0600 Subject: [PATCH 32/35] Add a key --- src/components/views/rooms/RoomList2.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 606f2d60e9..5d7b8ad2c6 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -206,6 +206,7 @@ export default class RoomList2 extends React.Component { avatar={avatar} notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)} onClick={openGroup} + key={`temporaryGroupTile_${g.groupId}`} /> ); }); From a6586120785f345a31d4ce7ca3a6c383cfe3b6bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 19:48:06 +0100 Subject: [PATCH 33/35] Add click-to-jump on badge in the room sublist header Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/NotificationBadge.tsx | 35 ++++++++++++++----- src/components/views/rooms/RoomSublist2.tsx | 33 ++++++++++++++++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 2111310555..31f1ea2021 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -29,6 +29,8 @@ import { IDestroyable } from "../../../utils/IDestroyable"; import SettingsStore from "../../../settings/SettingsStore"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { readReceiptChangeIsFor } from "../../../utils/read-receipts"; +import AccessibleButton from "../elements/AccessibleButton"; +import { XOR } from "../../../@types/common"; export const NOTIFICATION_STATE_UPDATE = "update"; @@ -62,11 +64,18 @@ interface IProps { roomId?: string; } +interface IClickableProps extends IProps, React.InputHTMLAttributes { + /** + * If specified will return an AccessibleButton instead of a div. + */ + onClick?(ev: React.MouseEvent); +} + interface IState { showCounts: boolean; // whether or not to show counts. Independent of props.forceCount } -export default class NotificationBadge extends React.PureComponent { +export default class NotificationBadge extends React.PureComponent, IState> { private countWatcherRef: string; constructor(props: IProps) { @@ -109,20 +118,22 @@ export default class NotificationBadge extends React.PureComponent= NotificationColor.Red; - const hasCount = this.props.notification.color >= NotificationColor.Grey; - const hasUnread = this.props.notification.color >= NotificationColor.Bold; + // Don't show a badge if we don't need to + if (notification.color <= NotificationColor.None) return null; + + const hasNotif = notification.color >= NotificationColor.Red; + const hasCount = notification.color >= NotificationColor.Grey; + const hasUnread = notification.color >= NotificationColor.Bold; const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif; let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); - if (this.props.forceCount) { + if (forceCount) { isEmptyBadge = false; if (!hasCount) return null; // Can't render a badge } - let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count); + let symbol = notification.symbol || formatMinimalBadgeCount(notification.count); if (isEmptyBadge) symbol = ""; const classes = classNames({ @@ -134,6 +145,14 @@ export default class NotificationBadge extends React.PureComponent 2, }); + if (onClick) { + return ( + + {symbol} + + ); + } + return (
{symbol} diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c64d62ebea..dfd6cdaefa 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -33,6 +33,7 @@ import StyledRadioButton from "../elements/StyledRadioButton"; import RoomListStore from "../../../stores/room-list/RoomListStore2"; import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import dis from "../../../dispatcher/dispatcher"; // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -160,6 +161,29 @@ export default class RoomSublist2 extends React.Component { this.forceUpdate(); // because the layout doesn't trigger a re-render }; + private onBadgeClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + let room; + if (this.props.tagId === DefaultTagID.Invite) { + // switch to first room in sortedList as that'll be the top of the list for the user + room = this.props.rooms && this.props.rooms[0]; + } else { + room = this.props.rooms.find((r: Room) => { + const notifState = this.state.notificationState.getForRoom(r); + return notifState.count > 0 && notifState.color === this.state.notificationState.color; + }); + } + + if (room) { + dis.dispatch({ + action: 'view_room', + room_id: room.roomId, + }); + } + }; + private onHeaderClick = (ev: React.MouseEvent) => { let target = ev.target as HTMLDivElement; if (!target.classList.contains('mx_RoomSublist2_headerText')) { @@ -287,7 +311,14 @@ export default class RoomSublist2 extends React.Component { // TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180 const tabIndex = isActive ? 0 : -1; - const badge = ; + const badge = ( + + ); let addRoomButton = null; if (!!this.props.onAddRoom) { From ae2a6ebc07a4f3ed02f673322a7d65bf277c08c7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 19:56:41 +0100 Subject: [PATCH 34/35] improve comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSublist2.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index dfd6cdaefa..5584b8a521 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -167,9 +167,10 @@ export default class RoomSublist2 extends React.Component { let room; if (this.props.tagId === DefaultTagID.Invite) { - // switch to first room in sortedList as that'll be the top of the list for the user + // switch to first room as that'll be the top of the list for the user room = this.props.rooms && this.props.rooms[0]; } else { + // find the first room with a count of the same colour as the badge count room = this.props.rooms.find((r: Room) => { const notifState = this.state.notificationState.getForRoom(r); return notifState.count > 0 && notifState.color === this.state.notificationState.color; From b65972d44f6e21ad59b4639b12c0cce779060106 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:23:20 -0600 Subject: [PATCH 35/35] Fix indentation --- src/stores/room-list/RoomListStore2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index d0ef88949c..e5205f6051 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -187,7 +187,7 @@ export class RoomListStore2 extends AsyncStore { const tryUpdate = async (updatedRoom: Room) => { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` + - ` in ${updatedRoom.roomId}`); + ` in ${updatedRoom.roomId}`); if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);