mirror of https://github.com/vector-im/riot-web
Merge branches 'develop' and 't3chguy/notifications0' of github.com:matrix-org/matrix-react-sdk into t3chguy/notifications0
commit
cf3c914382
|
@ -428,6 +428,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
border-radius: 8px;
|
||||
padding: 0px;
|
||||
box-shadow: none;
|
||||
|
||||
/* Don't show scroll-bars on spinner dialogs */
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
// TODO: Review mx_GeneralButton usage to see if it can use a different class
|
||||
|
@ -596,14 +600,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu_optionList {
|
||||
// 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: 20px;
|
||||
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.
|
||||
|
@ -634,7 +638,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 20px 0 0;
|
||||
padding: 12px 0 0;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
text-decoration: none;
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_TopLeftMenuButton.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
@import "./structures/_UserMenuButton.scss";
|
||||
@import "./structures/_UserMenu.scss";
|
||||
@import "./structures/_ViewSource.scss";
|
||||
@import "./structures/auth/_CompleteSecurity.scss";
|
||||
@import "./structures/auth/_Login.scss";
|
||||
|
|
|
@ -38,6 +38,12 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
// TagPanel handles its own CSS
|
||||
}
|
||||
|
||||
&:not(.mx_LeftPanel2_hasTagPanel) {
|
||||
.mx_LeftPanel2_roomListContainer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||
.mx_LeftPanel2_roomListContainer {
|
||||
|
@ -48,13 +54,13 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
flex-direction: column;
|
||||
|
||||
.mx_LeftPanel2_userHeader {
|
||||
padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom
|
||||
padding: 12px 12px 20px; // 12px top, 12px sides, 20px bottom
|
||||
|
||||
// Create another flexbox column for the rows to stack within
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs
|
||||
// This is basically just breadcrumbs. The row above that is handled by the UserMenu
|
||||
.mx_LeftPanel2_headerRow {
|
||||
// Create yet another flexbox, this time within the row, to ensure items stay
|
||||
// aligned correctly. This is also a row-based flexbox.
|
||||
|
@ -62,31 +68,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_userAvatarContainer {
|
||||
position: relative; // to make default avatars work
|
||||
margin-right: 8px;
|
||||
|
||||
.mx_LeftPanel2_userAvatar {
|
||||
border-radius: 32px; // should match avatar size
|
||||
}
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_userName {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
flex: 1;
|
||||
|
||||
// Ellipsize any text overflow
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_headerButtons {
|
||||
// No special styles: the rest of the layout happens to make it work.
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_breadcrumbsContainer {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
@ -152,21 +133,16 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
|||
min-width: unset;
|
||||
|
||||
// We have to forcefully set the width to override the resizer's style attribute.
|
||||
width: calc(68px + $tagPanelWidth) !important;
|
||||
&.mx_LeftPanel2_hasTagPanel {
|
||||
width: calc(68px + $tagPanelWidth) !important;
|
||||
}
|
||||
&:not(.mx_LeftPanel2_hasTagPanel) {
|
||||
width: 68px !important;
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_roomListContainer {
|
||||
width: 68px;
|
||||
|
||||
.mx_LeftPanel2_userHeader {
|
||||
.mx_LeftPanel2_headerRow {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_userAvatarContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_LeftPanel2_filterContainer {
|
||||
// Organize the flexbox into a centered column layout
|
||||
flex-direction: column;
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_UserMenuButton {
|
||||
> span {
|
||||
.mx_UserMenu {
|
||||
.mx_UserMenu_headerButtons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
|
@ -35,22 +35,71 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_row {
|
||||
// Create a row-based flexbox to ensure items stay aligned correctly.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_UserMenu_userAvatarContainer {
|
||||
position: relative; // to make default avatars work
|
||||
margin-right: 8px;
|
||||
height: 32px; // to remove the unknown 4px gap the browser puts below it
|
||||
|
||||
.mx_UserMenu_userAvatar {
|
||||
border-radius: 32px; // should match avatar size
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_userName {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
flex: 1;
|
||||
|
||||
// Ellipsize any text overflow
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_UserMenu_headerButtons {
|
||||
// No special styles: the rest of the layout happens to make it work.
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_UserMenu_minimized {
|
||||
.mx_UserMenu_userHeader {
|
||||
.mx_UserMenu_row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_UserMenu_userAvatarContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_contextMenu {
|
||||
.mx_UserMenu_contextMenu {
|
||||
width: 247px;
|
||||
|
||||
.mx_UserMenuButton_contextMenu_header {
|
||||
.mx_UserMenu_contextMenu_redRow {
|
||||
.mx_AccessibleButton {
|
||||
color: $warning-color !important; // !important to override styles from context menu
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu_icon::before {
|
||||
background-color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_contextMenu_header {
|
||||
// Create a flexbox to organize the header a bit easier
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:nth-child(n + 1) {
|
||||
// The first header will have appropriate padding, subsequent ones need a margin.
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_contextMenu_name {
|
||||
.mx_UserMenu_contextMenu_name {
|
||||
// Create another flexbox of columns to handle large user IDs
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -67,19 +116,19 @@ limitations under the License.
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_contextMenu_displayName {
|
||||
.mx_UserMenu_contextMenu_displayName {
|
||||
font-weight: bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_contextMenu_userId {
|
||||
.mx_UserMenu_contextMenu_userId {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_contextMenu_themeButton {
|
||||
.mx_UserMenu_contextMenu_themeButton {
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
width: 32px;
|
||||
|
@ -113,31 +162,31 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconHome::before {
|
||||
.mx_UserMenu_iconHome::before {
|
||||
mask-image: url('$(res)/img/feather-customised/home.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconBell::before {
|
||||
.mx_UserMenu_iconBell::before {
|
||||
mask-image: url('$(res)/img/feather-customised/notifications.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconLock::before {
|
||||
.mx_UserMenu_iconLock::before {
|
||||
mask-image: url('$(res)/img/feather-customised/lock.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconSettings::before {
|
||||
.mx_UserMenu_iconSettings::before {
|
||||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconArchive::before {
|
||||
.mx_UserMenu_iconArchive::before {
|
||||
mask-image: url('$(res)/img/feather-customised/archive.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconMessage::before {
|
||||
.mx_UserMenu_iconMessage::before {
|
||||
mask-image: url('$(res)/img/feather-customised/message-circle.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenuButton_iconSignOut::before {
|
||||
.mx_UserMenu_iconSignOut::before {
|
||||
mask-image: url('$(res)/img/feather-customised/sign-out.svg');
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
display: inline;
|
||||
}
|
||||
|
||||
.mx_InlineSpinner img {
|
||||
.mx_InlineSpinner_spin img {
|
||||
margin: 0px 6px;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,16 @@ limitations under the License.
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_Spinner_spin img {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MatrixChat_middlePanel .mx_Spinner {
|
||||
height: auto;
|
||||
}
|
||||
|
|
|
@ -77,8 +77,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
&:checked:disabled + label > .mx_Checkbox_background {
|
||||
background-color: $muted-fg-color;
|
||||
border-color: rgba($muted-fg-color, 0.5);
|
||||
background-color: $accent-color;
|
||||
border-color: $accent-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,23 +85,30 @@ limitations under the License.
|
|||
// ***************************
|
||||
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
opacity: 0.8;
|
||||
width: 16px;
|
||||
margin-right: 5px; // aligns with the room tile's badge
|
||||
|
||||
// Create another flexbox row because it's super easy to position the badge this way.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// Apply the width and margin to the badge so the container doesn't occupy dead space
|
||||
.mx_NotificationBadge {
|
||||
width: 16px;
|
||||
margin-left: 8px; // same as menu+aux buttons
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
.mx_NotificationBadge {
|
||||
margin-right: 4px; // just to push it over a bit, aligning it with the other elements
|
||||
}
|
||||
}
|
||||
|
||||
// Both of these buttons are hidden by default until the list is hovered
|
||||
.mx_RoomSublist2_auxButton,
|
||||
.mx_RoomSublist2_menuButton {
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
margin-left: 8px; // should be the same as the notification badge
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 32px;
|
||||
|
||||
&::before {
|
||||
|
@ -118,6 +125,13 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
// Hide the menu button by default
|
||||
.mx_RoomSublist2_menuButton {
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/plus.svg');
|
||||
}
|
||||
|
@ -130,9 +144,9 @@ limitations under the License.
|
|||
flex: 1;
|
||||
max-width: calc(100% - 16px); // 16px is the badge width
|
||||
text-transform: uppercase;
|
||||
opacity: 0.5;
|
||||
line-height: $font-16px;
|
||||
font-size: $font-12px;
|
||||
font-weight: 600;
|
||||
|
||||
// Ellipsize any text overflow
|
||||
text-overflow: ellipsis;
|
||||
|
@ -142,11 +156,9 @@ limitations under the License.
|
|||
.mx_RoomSublist2_collapseBtn {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
// Default hidden
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
height: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
@ -158,7 +170,7 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
background-color: $primary-fg-color;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
|
||||
|
@ -226,6 +238,16 @@ limitations under the License.
|
|||
.mx_RoomSublist2_showLessButtonChevron {
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
|
||||
}
|
||||
|
||||
&.mx_RoomSublist2_isCutting::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
box-shadow: 0px -2px 3px rgba(46, 47, 50, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
// Class name comes from the ResizableBox component
|
||||
|
@ -233,69 +255,34 @@ limitations under the License.
|
|||
// so that selector is below and one level higher.
|
||||
.react-resizable-handle {
|
||||
cursor: ns-resize;
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
|
||||
// Update RESIZE_HANDLE_HEIGHT if this changes
|
||||
height: 4px;
|
||||
|
||||
// This is positioned directly below the 'show more' button.
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
// This is to visually align the bar in the list. Should be 12px from
|
||||
// either side of the list. We define this after the positioning to
|
||||
// trick the browser.
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
// Together, these make the bar 64px wide
|
||||
left: calc(50% - 32px);
|
||||
right: calc(50% - 32px);
|
||||
}
|
||||
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.react-resizable-handle {
|
||||
opacity: 0.8;
|
||||
background-color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The aforementioned selector for the hover state.
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.react-resizable-handle {
|
||||
opacity: 0.2;
|
||||
|
||||
// Update the render() function for RoomSublist2 if this changes
|
||||
border: 2px solid $primary-fg-color;
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer {
|
||||
// If the header doesn't have an aux button we still need to hide the badge for
|
||||
// the menu button.
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
// Completely hide the badge
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
// The menu button will be the rightmost button, so make it correctly aligned.
|
||||
.mx_RoomSublist2_menuButton {
|
||||
margin-right: 1px; // line it up with the badges on the room tiles
|
||||
}
|
||||
}
|
||||
|
||||
// Both of these buttons have circled backgrounds and are visible at this point,
|
||||
// so make them so.
|
||||
.mx_RoomSublist2_auxButton,
|
||||
.mx_RoomSublist2_menuButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 16px;
|
||||
visibility: visible;
|
||||
background-color: $roomlist2-button-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerContainer {
|
||||
.mx_RoomSublist2_headerText {
|
||||
.mx_RoomSublist2_collapseBtn {
|
||||
visibility: visible;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
&.mx_RoomSublist2_hasMenuOpen,
|
||||
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
visibility: visible;
|
||||
width: 24px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,7 +331,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.mx_RoomSublist2_hasMenuOpen,
|
||||
& > .mx_RoomSublist2_headerContainer:hover {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
|
@ -365,7 +357,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
&.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
.mx_RoomSublist2_menuButton {
|
||||
bottom: 8px; // align to the middle of name, 40px less than the `bottom` above.
|
||||
}
|
||||
|
@ -374,27 +366,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
// We have a hover style on the room list with no specific list hovered, so account for that
|
||||
.mx_RoomList2:hover .mx_RoomSublist2:not(.mx_RoomSublist2_minimized),
|
||||
.mx_RoomSublist2_hasMenuOpen:not(.mx_RoomSublist2_minimized) {
|
||||
.mx_RoomSublist2_headerContainer_withAux {
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
// Completely hide the badge
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton {
|
||||
// Show the aux button, but not the list button
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 1px; // line it up with the badges on the room tiles
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_contextMenu {
|
||||
padding: 20px 16px;
|
||||
width: 250px;
|
||||
|
@ -404,6 +375,7 @@ limitations under the License.
|
|||
margin-bottom: 16px;
|
||||
margin-right: 16px; // additional 16px
|
||||
border: 1px solid $roomsublist2-divider-color;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_contextMenu_title {
|
||||
|
|
|
@ -209,9 +209,15 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_Advanced {
|
||||
color: $primary-fg-color;
|
||||
|
||||
> * {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_AdvancedToggle {
|
||||
color: $accent-color;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_systemFont {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.60236 3.67346C3.10764 5.59313 1.5 8.60882 1.5 12C1.5 17.799 6.20101 22.5 12 22.5C17.799 22.5 22.5 17.799 22.5 12C22.5 8.6452 20.9267 5.65787 18.4776 3.73562L17.7648 4.44842C20.0354 6.18437 21.5 8.92114 21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C6.75329 21.5 2.5 17.2467 2.5 12C2.5 8.88471 3.9995 6.11966 6.31612 4.38722L5.60236 3.67346Z" fill="#03b381"/>
|
||||
</svg>
|
After Width: | Height: | Size: 508 B |
|
@ -113,7 +113,7 @@ $theme-button-bg-color: #e3e8f0;
|
|||
$roomlist2-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
|
||||
$roomlist2-bg-color: $header-panel-bg-color;
|
||||
|
||||
$roomsublist2-divider-color: #e9eaeb;
|
||||
$roomsublist2-divider-color: $primary-fg-color;
|
||||
|
||||
$roomtile2-preview-color: #9e9e9e;
|
||||
$roomtile2-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -180,7 +180,7 @@ $theme-button-bg-color: #e3e8f0;
|
|||
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
|
||||
$roomlist2-bg-color: $header-panel-bg-color;
|
||||
|
||||
$roomsublist2-divider-color: #e9eaeb;
|
||||
$roomsublist2-divider-color: $primary-fg-color;
|
||||
|
||||
$roomtile2-preview-color: #9e9e9e;
|
||||
$roomtile2-default-badge-bg-color: #61708b;
|
||||
|
|
|
@ -25,8 +25,8 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
|||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
|
||||
export const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
export const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||
|
||||
export enum UpdateCheckStatus {
|
||||
Checking = "CHECKING",
|
||||
|
@ -221,7 +221,7 @@ export default abstract class BasePlatform {
|
|||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = fragmentAfterLogin || "";
|
||||
return url;
|
||||
|
@ -235,9 +235,9 @@ export default abstract class BasePlatform {
|
|||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
|
||||
// persist hs url and is url for when the user is returned to the app with the login token
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
if (mxClient.getIdentityServerUrl()) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
}
|
||||
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
|
|
|
@ -41,7 +41,10 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
||||
import DeviceListener from "./DeviceListener";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -164,8 +167,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
|||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const homeserver = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const identityServer = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
|
||||
const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY);
|
||||
if (!homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
return Promise.resolve(false);
|
||||
|
|
|
@ -265,22 +265,13 @@ function textForServerACLEvent(ev) {
|
|||
return text + changes.join(" ");
|
||||
}
|
||||
|
||||
function textForMessageEvent(ev, skipUserPrefix) {
|
||||
function textForMessageEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (skipUserPrefix) {
|
||||
message = ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('sent an image.');
|
||||
}
|
||||
} else {
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
@ -621,8 +612,8 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
stateHandlers[evType] = textForMjolnirEvent;
|
||||
}
|
||||
|
||||
export function textForEvent(ev, skipUserPrefix) {
|
||||
export function textForEvent(ev) {
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
if (handler) return handler(ev, skipUserPrefix);
|
||||
if (handler) return handler(ev);
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -22,18 +22,14 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import { _t } from "../../languageHandler";
|
||||
import RoomList2 from "../views/rooms/RoomList2";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import UserMenuButton from "./UserMenuButton";
|
||||
import UserMenu from "./UserMenu";
|
||||
import RoomSearch from "./RoomSearch";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
|
||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { throttle } from 'lodash';
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -51,10 +47,12 @@ interface IProps {
|
|||
interface IState {
|
||||
searchFilter: string; // TODO: Move search into room list?
|
||||
showBreadcrumbs: boolean;
|
||||
showTagPanel: boolean;
|
||||
}
|
||||
|
||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||
private tagPanelWatcherRef: string;
|
||||
|
||||
// TODO: Properly support TagPanel
|
||||
// TODO: Properly support searching/filtering
|
||||
|
@ -69,39 +67,25 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
this.state = {
|
||||
searchFilter: "",
|
||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
|
||||
};
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||
});
|
||||
|
||||
// We watch the middle panel because we don't actually get resized, the middle panel does.
|
||||
// We listen to the noisy channel to avoid choppy reaction times.
|
||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
// TSLint wants this to be a member, but we don't want that.
|
||||
// tslint:disable-next-line
|
||||
private onRoomStateUpdate = throttle((ev: MatrixEvent) => {
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onProfileUpdate();
|
||||
}
|
||||
}, 200, {trailing: true, leading: true});
|
||||
|
||||
private onProfileUpdate = async () => {
|
||||
// the store triggered an update, so force a layout update. We don't
|
||||
// have any state to store here for that to magically happen.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onSearch = (term: string): void => {
|
||||
this.setState({searchFilter: term});
|
||||
};
|
||||
|
@ -170,7 +154,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
// TODO: Presence
|
||||
// TODO: Breadcrumbs toggle
|
||||
// TODO: Menu button
|
||||
const avatarSize = 32; // should match border-radius of the avatar
|
||||
|
||||
let breadcrumbs;
|
||||
if (this.state.showBreadcrumbs) {
|
||||
|
@ -181,34 +164,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let name = <span className="mx_LeftPanel2_userName">{OwnProfileStore.instance.displayName}</span>;
|
||||
let buttons = (
|
||||
<span className="mx_LeftPanel2_headerButtons">
|
||||
<UserMenuButton />
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_LeftPanel2_userHeader">
|
||||
<div className="mx_LeftPanel2_headerRow">
|
||||
<span className="mx_LeftPanel2_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
|
||||
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_LeftPanel2_userAvatar"
|
||||
/>
|
||||
</span>
|
||||
{name}
|
||||
{buttons}
|
||||
</div>
|
||||
<UserMenu isMinimized={this.props.isMinimized} />
|
||||
{breadcrumbs}
|
||||
</div>
|
||||
);
|
||||
|
@ -231,7 +189,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const tagPanel = (
|
||||
const tagPanel = !this.state.showTagPanel ? null : (
|
||||
<div className="mx_LeftPanel2_tagPanelContainer">
|
||||
<TagPanel/>
|
||||
</div>
|
||||
|
@ -252,6 +210,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
|
||||
const containerClasses = classNames({
|
||||
"mx_LeftPanel2": true,
|
||||
"mx_LeftPanel2_hasTagPanel": !!tagPanel,
|
||||
"mx_LeftPanel2_minimized": this.props.isMinimized,
|
||||
});
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ interface IState {
|
|||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
class LoggedInView extends React.PureComponent<IProps, IState> {
|
||||
class LoggedInView extends React.Component<IProps, IState> {
|
||||
static displayName = 'LoggedInView';
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
// @ts-ignore - XXX: no idea why this import fails
|
||||
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";
|
||||
|
@ -1612,6 +1614,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
} else if (screen === 'directory') {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
} else if (screen === "start_sso" || screen === "start_cas") {
|
||||
// TODO if logged in, skip SSO
|
||||
let cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
cli = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const type = screen === "start_sso" ? "sso" : "cas";
|
||||
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
|
||||
} else if (screen === 'groups') {
|
||||
dis.dispatch({
|
||||
action: 'view_my_groups',
|
||||
|
@ -1915,9 +1930,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.onLoggedIn();
|
||||
};
|
||||
|
||||
render() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
getFragmentAfterLogin() {
|
||||
let fragmentAfterLogin = "";
|
||||
if (this.props.initialScreenAfterLogin &&
|
||||
// XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop
|
||||
|
@ -1925,7 +1938,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
) {
|
||||
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
|
||||
}
|
||||
return fragmentAfterLogin;
|
||||
}
|
||||
|
||||
render() {
|
||||
const fragmentAfterLogin = this.getFragmentAfterLogin();
|
||||
let view;
|
||||
|
||||
if (this.state.view === Views.LOADING) {
|
||||
|
@ -2004,7 +2021,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
} else if (this.state.view === Views.WELCOME) {
|
||||
const Welcome = sdk.getComponent('auth.Welcome');
|
||||
view = <Welcome {...this.getServerProperties()} fragmentAfterLogin={fragmentAfterLogin} />;
|
||||
view = <Welcome />;
|
||||
} else if (this.state.view === Views.REGISTER) {
|
||||
const Registration = sdk.getComponent('structures.auth.Registration');
|
||||
view = (
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
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 * as React from "react";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { createRef } from "react";
|
||||
import { _t } from "../../languageHandler";
|
||||
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
|
||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
menuDisplayed: boolean;
|
||||
isDarkTheme: boolean;
|
||||
}
|
||||
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private isUserOnDarkTheme(): boolean {
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme.startsWith("custom-")) {
|
||||
return getCustomTheme(theme.substring("custom-".length)).is_dark;
|
||||
}
|
||||
return theme === "dark";
|
||||
}
|
||||
|
||||
private onProfileUpdate = async () => {
|
||||
// the store triggered an update, so force a layout update. We don't
|
||||
// have any state to store here for that to magically happen.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onThemeChanged = () => {
|
||||
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
|
||||
};
|
||||
|
||||
private onAction = (ev: ActionPayload) => {
|
||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
||||
|
||||
// For accessibility
|
||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: InputEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({menuDisplayed: true});
|
||||
};
|
||||
|
||||
private onCloseMenu = (ev: InputEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({menuDisplayed: false});
|
||||
};
|
||||
|
||||
private onSwitchThemeClick = () => {
|
||||
// Disable system theme matching if the user hits this button
|
||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||
|
||||
const newTheme = this.state.isDarkTheme ? "light" : "dark";
|
||||
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
|
||||
};
|
||||
|
||||
private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onShowArchived = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view (deferred)
|
||||
console.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignOutClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
};
|
||||
|
||||
private renderContextMenu = (): React.ReactNode => {
|
||||
if (!this.state.menuDisplayed) return null;
|
||||
|
||||
let hostingLink;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => (
|
||||
<a
|
||||
href={signupLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
tabIndex={-1}
|
||||
>{sub}</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
if (this.hasHomePage) {
|
||||
homeButton = (
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onHomeClick}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
|
||||
<span>{_t("Home")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const elementRect = this.buttonRef.current.getBoundingClientRect();
|
||||
return (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
left={elementRect.width + elementRect.left}
|
||||
top={elementRect.top + elementRect.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
>
|
||||
<div className="mx_IconizedContextMenu mx_UserMenu_contextMenu">
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{MatrixClientPeg.get().getUserId()}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
||||
>
|
||||
<img
|
||||
src={require("../../../res/img/feather-customised/sun.svg")}
|
||||
alt={_t("Switch theme")}
|
||||
width={16}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hostingLink}
|
||||
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||
<ul>
|
||||
{homeButton}
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
|
||||
<span>{_t("Notification settings")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
|
||||
<span>{_t("Security & privacy")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
|
||||
<span>{_t("All settings")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onShowArchived}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
|
||||
<span>{_t("Archived rooms")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onProvideFeedback}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
|
||||
<span>{_t("Feedback")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mx_IconizedContextMenu_optionList">
|
||||
<ul>
|
||||
<li className="mx_UserMenu_contextMenu_redRow">
|
||||
<AccessibleButton onClick={this.onSignOutClick}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
|
||||
<span>{_t("Sign out")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const avatarSize = 32; // should match border-radius of the avatar
|
||||
|
||||
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
|
||||
let buttons = (
|
||||
<span className="mx_UserMenu_headerButtons">
|
||||
{/* masked image in CSS */}
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_UserMenu': true,
|
||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("Account settings")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
>
|
||||
<div className="mx_UserMenu_row">
|
||||
<span className="mx_UserMenu_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
|
||||
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar"
|
||||
/>
|
||||
</span>
|
||||
{name}
|
||||
{buttons}
|
||||
</div>
|
||||
{this.renderContextMenu()}
|
||||
</ContextMenuButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,294 +0,0 @@
|
|||
/*
|
||||
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 * as React from "react";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { createRef } from "react";
|
||||
import { _t } from "../../languageHandler";
|
||||
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
|
||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
menuDisplayed: boolean;
|
||||
isDarkTheme: boolean;
|
||||
}
|
||||
|
||||
export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private isUserOnDarkTheme(): boolean {
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme.startsWith("custom-")) {
|
||||
return getCustomTheme(theme.substring("custom-".length)).is_dark;
|
||||
}
|
||||
return theme === "dark";
|
||||
}
|
||||
|
||||
private onProfileUpdate = async () => {
|
||||
// the store triggered an update, so force a layout update. We don't
|
||||
// have any state to store here for that to magically happen.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onThemeChanged = () => {
|
||||
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
|
||||
};
|
||||
|
||||
private onAction = (ev: ActionPayload) => {
|
||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
||||
|
||||
// For accessibility
|
||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: InputEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({menuDisplayed: true});
|
||||
};
|
||||
|
||||
private onCloseMenu = () => {
|
||||
this.setState({menuDisplayed: false});
|
||||
};
|
||||
|
||||
private onSwitchThemeClick = () => {
|
||||
// Disable system theme matching if the user hits this button
|
||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||
|
||||
const newTheme = this.state.isDarkTheme ? "light" : "dark";
|
||||
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
|
||||
};
|
||||
|
||||
private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onShowArchived = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view (deferred)
|
||||
console.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignOutClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
||||
this.setState({menuDisplayed: false}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
};
|
||||
|
||||
public render() {
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
let hostingLink;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
<div className="mx_UserMenuButton_contextMenu_header">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => (
|
||||
<a
|
||||
href={signupLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
tabIndex={-1}
|
||||
>{sub}</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
if (this.hasHomePage) {
|
||||
homeButton = (
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onHomeClick}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconHome" />
|
||||
<span>{_t("Home")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const elementRect = this.buttonRef.current.getBoundingClientRect();
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
left={elementRect.left}
|
||||
top={elementRect.top + elementRect.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
>
|
||||
<div className="mx_IconizedContextMenu mx_UserMenuButton_contextMenu">
|
||||
<div className="mx_UserMenuButton_contextMenu_header">
|
||||
<div className="mx_UserMenuButton_contextMenu_name">
|
||||
<span className="mx_UserMenuButton_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenuButton_contextMenu_userId">
|
||||
{MatrixClientPeg.get().getUserId()}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx_UserMenuButton_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
||||
>
|
||||
<img
|
||||
src={require("../../../res/img/feather-customised/sun.svg")}
|
||||
alt={_t("Switch theme")}
|
||||
width={16}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hostingLink}
|
||||
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||
<ul>
|
||||
{homeButton}
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconBell" />
|
||||
<span>{_t("Notification settings")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconLock" />
|
||||
<span>{_t("Security & privacy")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconSettings" />
|
||||
<span>{_t("All settings")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onShowArchived}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconArchive" />
|
||||
<span>{_t("Archived rooms")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onProvideFeedback}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconMessage" />
|
||||
<span>{_t("Feedback")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mx_IconizedContextMenu_optionList">
|
||||
<ul>
|
||||
<li>
|
||||
<AccessibleButton onClick={this.onSignOutClick}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconSignOut" />
|
||||
<span>{_t("Sign out")}</span>
|
||||
</AccessibleButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className="mx_UserMenuButton"
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("Account settings")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
>
|
||||
<span>{/* masked image in CSS */}</span>
|
||||
</ContextMenuButton>
|
||||
{contextMenu}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import {sendLoginRequest} from "../../../Login";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
|
@ -158,8 +158,8 @@ export default class SoftLogout extends React.Component {
|
|||
async trySsoLogin() {
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.token";
|
||||
const loginParams = {
|
||||
token: this.props.realQueryParams['loginToken'],
|
||||
|
|
|
@ -18,9 +18,7 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import {_td} from "../../../languageHandler";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
@ -39,15 +37,6 @@ export default class Welcome extends React.PureComponent {
|
|||
pageUrl = 'welcome.html';
|
||||
}
|
||||
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
const tmpClient = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
const plaf = PlatformPeg.get();
|
||||
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(),
|
||||
this.props.fragmentAfterLogin);
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<div className="mx_Welcome">
|
||||
|
@ -55,8 +44,8 @@ export default class Welcome extends React.PureComponent {
|
|||
className="mx_WelcomePage"
|
||||
url={pageUrl}
|
||||
replaceMap={{
|
||||
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
|
||||
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
|
||||
"$riot:ssoUrl": "#/start_sso",
|
||||
"$riot:casUrl": "#/start_cas",
|
||||
}}
|
||||
/>
|
||||
<LanguageSelector />
|
||||
|
|
|
@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import Spinner from './Spinner';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
@ -740,7 +740,7 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
<MessageSpinner msg='Loading...' />
|
||||
<Spinner message={_t("Loading...")} />
|
||||
</div>
|
||||
);
|
||||
if (!this.state.hasPermissionToLoad) {
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InlineSpinner',
|
||||
|
@ -25,9 +27,25 @@ export default createReactClass({
|
|||
const h = this.props.h || 16;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
|
||||
let divClass;
|
||||
let imageSource;
|
||||
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
|
||||
divClass = "mx_InlineSpinner mx_Spinner_spin";
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
divClass = "mx_InlineSpinner";
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
<div className={divClass}>
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClass}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MessageSpinner',
|
||||
|
||||
render: function() {
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
const msg = this.props.msg || "Loading...";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -30,6 +30,7 @@ interface IProps {
|
|||
isExplicit?: boolean;
|
||||
// XXX: once design replaces all toggles make this the default
|
||||
useCheckbox?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?(checked: boolean): void;
|
||||
}
|
||||
|
||||
|
@ -78,14 +79,23 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
else label = _t(label);
|
||||
|
||||
if (this.props.useCheckbox) {
|
||||
return <StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={!canChange} >
|
||||
return <StyledCheckbox
|
||||
checked={this.state.value}
|
||||
onChange={this.checkBoxOnChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
>
|
||||
{label}
|
||||
</StyledCheckbox>;
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_SettingsFlag">
|
||||
<span className="mx_SettingsFlag_label">{label}</span>
|
||||
<ToggleSwitch checked={this.state.value} onChange={this.onChange} disabled={!canChange} aria-label={label} />
|
||||
<ToggleSwitch
|
||||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,19 +16,39 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from "prop-types";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'Spinner',
|
||||
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
|
||||
let divClass;
|
||||
let imageSource;
|
||||
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
|
||||
divClass = "mx_Spinner mx_Spinner_spin";
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
divClass = "mx_Spinner";
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className={divClass}>
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message}</div> </React.Fragment> }
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClassName}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Spinner.propTypes = {
|
||||
w: PropTypes.number,
|
||||
h: PropTypes.number,
|
||||
imgClassName: PropTypes.string,
|
||||
message: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
|
|
@ -22,6 +22,7 @@ import MFileBody from './MFileBody';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default class MAudioBody extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -94,7 +95,7 @@ export default class MAudioBody extends React.Component {
|
|||
// Not sure how tall the audio player is so not sure how tall it should actually be.
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
|
||||
<InlineSpinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -365,12 +366,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <img
|
||||
src={require("../../../../res/img/spinner.gif")}
|
||||
alt={content.body}
|
||||
width="32"
|
||||
height="32"
|
||||
/>;
|
||||
placeholder = <InlineSpinner w={32} h={32} />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
|
|
|
@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -147,7 +148,7 @@ export default createReactClass({
|
|||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
|
||||
<InlineSpinner />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -70,6 +70,7 @@ interface IProps {
|
|||
interface IState {
|
||||
notificationState: ListNotificationState;
|
||||
menuDisplayed: boolean;
|
||||
isResizing: boolean;
|
||||
}
|
||||
|
||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
|
@ -82,6 +83,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
this.state = {
|
||||
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
|
||||
menuDisplayed: false,
|
||||
isResizing: false,
|
||||
};
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
}
|
||||
|
@ -111,13 +113,21 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private onResizeStart = () => {
|
||||
this.setState({isResizing: true});
|
||||
};
|
||||
|
||||
private onResizeStop = () => {
|
||||
this.setState({isResizing: false});
|
||||
};
|
||||
|
||||
private onShowAllClick = () => {
|
||||
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private onShowLessClick = () => {
|
||||
this.props.layout.visibleTiles = this.props.layout.minVisibleTiles;
|
||||
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
|
@ -320,8 +330,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<span>{this.props.label}</span>
|
||||
</AccessibleButton>
|
||||
{this.renderMenu()}
|
||||
{this.props.isMinimized ? null : addRoomButton}
|
||||
{this.props.isMinimized ? null : badgeContainer}
|
||||
{this.props.isMinimized ? null : addRoomButton}
|
||||
</div>
|
||||
{this.props.isMinimized ? badgeContainer : null}
|
||||
{this.props.isMinimized ? addRoomButton : null}
|
||||
|
@ -356,6 +366,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
const nVisible = Math.floor(layout.visibleTiles);
|
||||
const visibleTiles = tiles.slice(0, nVisible);
|
||||
|
||||
const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length);
|
||||
const showMoreBtnClasses = classNames({
|
||||
'mx_RoomSublist2_showNButton': true,
|
||||
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
||||
});
|
||||
|
||||
// If we're hiding rooms, show a 'show more' button to the user. This button
|
||||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
|
@ -370,7 +386,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (this.props.isMinimized) showMoreText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showNButton'>
|
||||
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
||||
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
|
@ -386,7 +402,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (this.props.isMinimized) showLessText = null;
|
||||
showNButton = (
|
||||
<div onClick={this.onShowLessClick} className='mx_RoomSublist2_showNButton'>
|
||||
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
||||
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
|
@ -432,6 +448,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
resizeHandles={handles}
|
||||
onResize={this.onResize}
|
||||
className="mx_RoomSublist2_resizeBox"
|
||||
onResizeStart={this.onResizeStart}
|
||||
onResizeStop={this.onResizeStop}
|
||||
>
|
||||
{visibleTiles}
|
||||
{showNButton}
|
||||
|
|
|
@ -34,7 +34,7 @@ import NotificationBadge, {
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import RoomTileIcon from "./RoomTileIcon";
|
||||
|
||||
/*******************************************************************
|
||||
|
@ -271,7 +271,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
let messagePreview = null;
|
||||
if (this.props.showMessagePreview && !this.props.isMinimized) {
|
||||
// The preview store heavily caches this info, so should be safe to hammer.
|
||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room);
|
||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
|
||||
// Only show the preview if there is one to show.
|
||||
if (text) {
|
||||
|
|
|
@ -390,7 +390,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
let advanced: React.ReactNode;
|
||||
|
||||
if (this.state.showAdvanced) {
|
||||
advanced = <div>
|
||||
advanced = <>
|
||||
<SettingsFlag
|
||||
name="useCompactLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
disabled={this.state.useIRCLayout}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
|
@ -412,7 +418,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
disabled={!this.state.useSystemFont}
|
||||
value={this.state.systemFont}
|
||||
/>
|
||||
</div>;
|
||||
</>;
|
||||
}
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
|
||||
{toggle}
|
||||
|
|
|
@ -247,7 +247,6 @@
|
|||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
|
||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
||||
"sent an image.": "sent an image.",
|
||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
||||
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
||||
|
@ -421,12 +420,66 @@
|
|||
"Restart": "Restart",
|
||||
"Upgrade your Riot": "Upgrade your Riot",
|
||||
"A new version of Riot is available!": "A new version of Riot is available!",
|
||||
"You: %(message)s": "You: %(message)s",
|
||||
"Guest": "Guest",
|
||||
"There was an error joining the room": "There was an error joining the room",
|
||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||
"Failed to join room": "Failed to join room",
|
||||
"You joined the call": "You joined the call",
|
||||
"%(senderName)s joined the call": "%(senderName)s joined the call",
|
||||
"Call in progress": "Call in progress",
|
||||
"You left the call": "You left the call",
|
||||
"%(senderName)s left the call": "%(senderName)s left the call",
|
||||
"Call ended": "Call ended",
|
||||
"You started a call": "You started a call",
|
||||
"%(senderName)s started a call": "%(senderName)s started a call",
|
||||
"Waiting for answer": "Waiting for answer",
|
||||
"%(senderName)s is calling": "%(senderName)s is calling",
|
||||
"You created the room": "You created the room",
|
||||
"%(senderName)s created the room": "%(senderName)s created the room",
|
||||
"You made the chat encrypted": "You made the chat encrypted",
|
||||
"%(senderName)s made the chat encrypted": "%(senderName)s made the chat encrypted",
|
||||
"You made history visible to new members": "You made history visible to new members",
|
||||
"%(senderName)s made history visible to new members": "%(senderName)s made history visible to new members",
|
||||
"You made history visible to anyone": "You made history visible to anyone",
|
||||
"%(senderName)s made history visible to anyone": "%(senderName)s made history visible to anyone",
|
||||
"You made history visible to future members": "You made history visible to future members",
|
||||
"%(senderName)s made history visible to future members": "%(senderName)s made history visible to future members",
|
||||
"You were invited": "You were invited",
|
||||
"%(targetName)s was invited": "%(targetName)s was invited",
|
||||
"You left": "You left",
|
||||
"%(targetName)s left": "%(targetName)s left",
|
||||
"You were kicked (%(reason)s)": "You were kicked (%(reason)s)",
|
||||
"%(targetName)s was kicked (%(reason)s)": "%(targetName)s was kicked (%(reason)s)",
|
||||
"You were kicked": "You were kicked",
|
||||
"%(targetName)s was kicked": "%(targetName)s was kicked",
|
||||
"You rejected the invite": "You rejected the invite",
|
||||
"%(targetName)s rejected the invite": "%(targetName)s rejected the invite",
|
||||
"You were uninvited": "You were uninvited",
|
||||
"%(targetName)s was uninvited": "%(targetName)s was uninvited",
|
||||
"You were banned (%(reason)s)": "You were banned (%(reason)s)",
|
||||
"%(targetName)s was banned (%(reason)s)": "%(targetName)s was banned (%(reason)s)",
|
||||
"You were banned": "You were banned",
|
||||
"%(targetName)s was banned": "%(targetName)s was banned",
|
||||
"You joined": "You joined",
|
||||
"%(targetName)s joined": "%(targetName)s joined",
|
||||
"You changed your name": "You changed your name",
|
||||
"%(targetName)s changed their name": "%(targetName)s changed their name",
|
||||
"You changed your avatar": "You changed your avatar",
|
||||
"%(targetName)s changed their avatar": "%(targetName)s changed their avatar",
|
||||
"%(senderName)s %(emote)s": "%(senderName)s %(emote)s",
|
||||
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
|
||||
"You changed the room name": "You changed the room name",
|
||||
"%(senderName)s changed the room name": "%(senderName)s changed the room name",
|
||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"You uninvited %(targetName)s": "You uninvited %(targetName)s",
|
||||
"%(senderName)s uninvited %(targetName)s": "%(senderName)s uninvited %(targetName)s",
|
||||
"You invited %(targetName)s": "You invited %(targetName)s",
|
||||
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
|
||||
"You changed the room topic": "You changed the room topic",
|
||||
"%(senderName)s changed the room topic": "%(senderName)s changed the room topic",
|
||||
"New spinner design": "New spinner design",
|
||||
"Font scaling": "Font scaling",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Custom user status messages": "Custom user status messages",
|
||||
|
@ -441,7 +494,7 @@
|
|||
"Font size": "Font size",
|
||||
"Use custom size": "Use custom size",
|
||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||
"Use compact timeline layout": "Use compact timeline layout",
|
||||
"Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout",
|
||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||
"Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)",
|
||||
"Show avatar changes": "Show avatar changes",
|
||||
|
|
|
@ -97,6 +97,12 @@ export const SETTINGS = {
|
|||
// // not use this for new settings.
|
||||
// invertedSettingName: "my-negative-setting",
|
||||
// },
|
||||
"feature_new_spinner": {
|
||||
isFeature: true,
|
||||
displayName: _td("New spinner design"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_font_scaling": {
|
||||
isFeature: true,
|
||||
displayName: _td("Font scaling"),
|
||||
|
@ -197,7 +203,7 @@ export const SETTINGS = {
|
|||
},
|
||||
"useCompactLayout": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Use compact timeline layout'),
|
||||
displayName: _td('Use a more compact ‘Modern’ layout'),
|
||||
default: false,
|
||||
},
|
||||
"showRedactions": {
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
|
||||
import { textForEvent } from "../TextForEvent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
const PREVIEWABLE_EVENTS = [
|
||||
// This is the same list from RiotX
|
||||
{type: "m.room.message", isState: false},
|
||||
{type: "m.room.name", isState: true},
|
||||
{type: "m.room.topic", isState: true},
|
||||
{type: "m.room.member", isState: true},
|
||||
{type: "m.room.history_visibility", isState: true},
|
||||
{type: "m.call.invite", isState: false},
|
||||
{type: "m.call.hangup", isState: false},
|
||||
{type: "m.call.answer", isState: false},
|
||||
{type: "m.room.encrypted", isState: false},
|
||||
{type: "m.room.encryption", isState: true},
|
||||
{type: "m.room.third_party_invite", isState: true},
|
||||
{type: "m.sticker", isState: false},
|
||||
{type: "m.room.create", isState: true},
|
||||
];
|
||||
|
||||
// The maximum number of events we're willing to look back on to get a preview.
|
||||
const MAX_EVENTS_BACKWARDS = 50;
|
||||
|
||||
interface IState {
|
||||
[roomId: string]: string | null; // null indicates the preview is empty
|
||||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
public static get instance(): MessagePreviewStore {
|
||||
return MessagePreviewStore.internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pre-translated preview for a given room
|
||||
* @param room The room to get the preview for.
|
||||
* @returns The preview, or null if none present.
|
||||
*/
|
||||
public getPreviewForRoom(room: Room): string {
|
||||
if (!room) return null; // invalid room, just return nothing
|
||||
|
||||
// It's faster to do a lookup this way than it is to use Object.keys().includes()
|
||||
// We only want to generate a preview if there's one actually missing and not explicitly
|
||||
// set as 'none'.
|
||||
const val = this.state[room.roomId];
|
||||
if (val !== null && typeof(val) !== "string") {
|
||||
this.generatePreview(room);
|
||||
}
|
||||
|
||||
return this.state[room.roomId];
|
||||
}
|
||||
|
||||
private generatePreview(room: Room) {
|
||||
const events = room.timeline;
|
||||
if (!events) return; // should only happen in tests
|
||||
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
|
||||
|
||||
const event = events[i];
|
||||
const preview = this.generatePreviewForEvent(event);
|
||||
if (preview.isPreviewable) {
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: preview.preview});
|
||||
return; // break - we found some text
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't find anything, subscribe ourselves to an update
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: null});
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
if (!this.matrixClient) return;
|
||||
|
||||
// TODO: Remove when new room list is made the default
|
||||
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
|
||||
|
||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||
const event = payload.event; // TODO: Type out the dispatcher
|
||||
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
|
||||
|
||||
const preview = this.generatePreviewForEvent(event);
|
||||
if (preview.isPreviewable) {
|
||||
await this.updateState({[event.getRoomId()]: preview.preview});
|
||||
return; // break - we found some text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } {
|
||||
if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) {
|
||||
const isSelf = event.getSender() === this.matrixClient.getUserId();
|
||||
let text = textForEvent(event, /*skipUserPrefix=*/isSelf);
|
||||
if (!text || text.trim().length === 0) text = null; // force null if useless to us
|
||||
if (text && isSelf) {
|
||||
// XXX: i18n doesn't really work here if the language doesn't support prefixing.
|
||||
// We'd ideally somehow route the `You:` bit to the textForEvent call, however
|
||||
// threading that through is non-trivial.
|
||||
text = _t("You: %(message)s", {message: text});
|
||||
}
|
||||
return {isPreviewable: true, preview: text};
|
||||
}
|
||||
return {isPreviewable: false, preview: null};
|
||||
}
|
||||
}
|
|
@ -18,6 +18,10 @@ import { TagID } from "./models";
|
|||
|
||||
const TILE_HEIGHT_PX = 44;
|
||||
|
||||
// the .65 comes from the CSS where the show more button is
|
||||
// mathematically 65% of a tile when floating.
|
||||
const RESIZER_BOX_FACTOR = 0.65;
|
||||
|
||||
interface ISerializedListLayout {
|
||||
numTiles: number;
|
||||
showPreviews: boolean;
|
||||
|
@ -67,6 +71,7 @@ export class ListLayout {
|
|||
}
|
||||
|
||||
public get visibleTiles(): number {
|
||||
if (this._n === 0) return this.defaultVisibleTiles;
|
||||
return Math.max(this._n, this.minVisibleTiles);
|
||||
}
|
||||
|
||||
|
@ -76,9 +81,13 @@ export class ListLayout {
|
|||
}
|
||||
|
||||
public get minVisibleTiles(): number {
|
||||
// the .65 comes from the CSS where the show more button is
|
||||
// mathematically 65% of a tile when floating.
|
||||
return 4.65;
|
||||
return 1 + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public get defaultVisibleTiles(): number {
|
||||
// TODO: Remove dogfood flag
|
||||
const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4);
|
||||
return val + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
|
||||
|
@ -92,6 +101,10 @@ export class ListLayout {
|
|||
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
|
||||
}
|
||||
|
||||
public tilesWithResizerBoxFactor(n: number): number {
|
||||
return n + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public tilesWithPadding(n: number, paddingPx: number): number {
|
||||
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy";
|
||||
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||
import { NameEventPreview } from "./previews/NameEventPreview";
|
||||
import { TagID } from "./models";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { TopicEventPreview } from "./previews/TopicEventPreview";
|
||||
import { MembershipEventPreview } from "./previews/MembershipEventPreview";
|
||||
import { HistoryVisibilityEventPreview } from "./previews/HistoryVisibilityEventPreview";
|
||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
||||
import { EncryptionEventPreview } from "./previews/EncryptionEventPreview";
|
||||
import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview";
|
||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||
import { CreationEventPreview } from "./previews/CreationEventPreview";
|
||||
|
||||
const PREVIEWS = {
|
||||
'm.room.message': {
|
||||
isState: false,
|
||||
previewer: new MessageEventPreview(),
|
||||
},
|
||||
'm.room.name': {
|
||||
isState: true,
|
||||
previewer: new NameEventPreview(),
|
||||
},
|
||||
'm.room.topic': {
|
||||
isState: true,
|
||||
previewer: new TopicEventPreview(),
|
||||
},
|
||||
'm.room.member': {
|
||||
isState: true,
|
||||
previewer: new MembershipEventPreview(),
|
||||
},
|
||||
'm.room.history_visibility': {
|
||||
isState: true,
|
||||
previewer: new HistoryVisibilityEventPreview(),
|
||||
},
|
||||
'm.call.invite': {
|
||||
isState: false,
|
||||
previewer: new CallInviteEventPreview(),
|
||||
},
|
||||
'm.call.answer': {
|
||||
isState: false,
|
||||
previewer: new CallAnswerEventPreview(),
|
||||
},
|
||||
'm.call.hangup': {
|
||||
isState: false,
|
||||
previewer: new CallHangupEvent(),
|
||||
},
|
||||
'm.room.encryption': {
|
||||
isState: true,
|
||||
previewer: new EncryptionEventPreview(),
|
||||
},
|
||||
'm.room.third_party_invite': {
|
||||
isState: true,
|
||||
previewer: new ThirdPartyInviteEventPreview(),
|
||||
},
|
||||
'm.sticker': {
|
||||
isState: false,
|
||||
previewer: new StickerEventPreview(),
|
||||
},
|
||||
'm.reaction': {
|
||||
isState: false,
|
||||
previewer: new ReactionEventPreview(),
|
||||
},
|
||||
'm.room.create': {
|
||||
isState: true,
|
||||
previewer: new CreationEventPreview(),
|
||||
},
|
||||
};
|
||||
|
||||
// The maximum number of events we're willing to look back on to get a preview.
|
||||
const MAX_EVENTS_BACKWARDS = 50;
|
||||
|
||||
// type merging ftw
|
||||
type TAG_ANY = "im.vector.any";
|
||||
const TAG_ANY: TAG_ANY = "im.vector.any";
|
||||
|
||||
interface IState {
|
||||
[roomId: string]: Map<TagID | TAG_ANY, string | null>; // null indicates the preview is empty / irrelevant
|
||||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
public static get instance(): MessagePreviewStore {
|
||||
return MessagePreviewStore.internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pre-translated preview for a given room
|
||||
* @param room The room to get the preview for.
|
||||
* @param inTagId The tag ID in which the room resides
|
||||
* @returns The preview, or null if none present.
|
||||
*/
|
||||
public getPreviewForRoom(room: Room, inTagId: TagID): string {
|
||||
if (!room) return null; // invalid room, just return nothing
|
||||
|
||||
const val = this.state[room.roomId];
|
||||
if (!val) this.generatePreview(room, inTagId);
|
||||
|
||||
const previews = this.state[room.roomId];
|
||||
if (!previews) return null;
|
||||
|
||||
if (!previews.has(inTagId)) {
|
||||
return previews.get(TAG_ANY);
|
||||
}
|
||||
return previews.get(inTagId);
|
||||
}
|
||||
|
||||
private generatePreview(room: Room, tagId?: TagID) {
|
||||
const events = room.timeline;
|
||||
if (!events) return; // should only happen in tests
|
||||
|
||||
let map = this.state[room.roomId];
|
||||
if (!map) {
|
||||
map = new Map<TagID | TAG_ANY, string | null>();
|
||||
|
||||
// We set the state later with the map, so no need to send an update now
|
||||
}
|
||||
|
||||
// Set the tags so we know what to generate
|
||||
if (!map.has(TAG_ANY)) map.set(TAG_ANY, null);
|
||||
if (tagId && !map.has(tagId)) map.set(tagId, null);
|
||||
|
||||
let changed = false;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
|
||||
|
||||
const event = events[i];
|
||||
const previewDef = PREVIEWS[event.getType()];
|
||||
if (!previewDef) continue;
|
||||
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue;
|
||||
|
||||
const anyPreview = previewDef.previewer.getTextFor(event, null);
|
||||
if (!anyPreview) continue; // not previewable for some reason
|
||||
|
||||
changed = changed || anyPreview !== map.get(TAG_ANY);
|
||||
map.set(TAG_ANY, anyPreview);
|
||||
|
||||
const tagsToGenerate = Array.from(map.keys()).filter(t => t !== TAG_ANY); // we did the any tag above
|
||||
for (const genTagId of tagsToGenerate) {
|
||||
const realTagId: TagID = genTagId === TAG_ANY ? null : genTagId;
|
||||
const preview = previewDef.previewer.getTextFor(event, realTagId);
|
||||
if (preview === anyPreview) {
|
||||
changed = changed || anyPreview !== map.get(genTagId);
|
||||
map.delete(genTagId);
|
||||
} else {
|
||||
changed = changed || preview !== map.get(genTagId);
|
||||
map.set(genTagId, preview);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// Update state for good measure - causes emit for update
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: map});
|
||||
}
|
||||
return; // we're done
|
||||
}
|
||||
|
||||
// At this point, we didn't generate a preview so clear it
|
||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
||||
this.updateState({[room.roomId]: null});
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload) {
|
||||
if (!this.matrixClient) return;
|
||||
|
||||
// TODO: Remove when new room list is made the default
|
||||
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
|
||||
|
||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||
const event = payload.event; // TODO: Type out the dispatcher
|
||||
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
|
||||
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallAnswerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
return _t("You joined the call");
|
||||
} else {
|
||||
return _t("%(senderName)s joined the call", {senderName: getSenderName(event)});
|
||||
}
|
||||
} else {
|
||||
return _t("Call in progress");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallHangupEvent implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
return _t("You left the call");
|
||||
} else {
|
||||
return _t("%(senderName)s left the call", {senderName: getSenderName(event)});
|
||||
}
|
||||
} else {
|
||||
return _t("Call ended");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
||||
return _t("You started a call");
|
||||
} else {
|
||||
return _t("%(senderName)s started a call", {senderName: getSenderName(event)});
|
||||
}
|
||||
} else {
|
||||
if (isSelf(event)) {
|
||||
return _t("Waiting for answer");
|
||||
} else {
|
||||
return _t("%(senderName)s is calling", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CreationEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You created the room");
|
||||
} else {
|
||||
return _t("%(senderName)s created the room", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class EncryptionEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You made the chat encrypted");
|
||||
} else {
|
||||
return _t("%(senderName)s made the chat encrypted", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class HistoryVisibilityEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const visibility = event.getContent()['history_visibility'];
|
||||
const isUs = isSelf(event);
|
||||
|
||||
if (visibility === 'invited' || visibility === 'joined') {
|
||||
return isUs
|
||||
? _t("You made history visible to new members")
|
||||
: _t("%(senderName)s made history visible to new members", {senderName: getSenderName(event)});
|
||||
} else if (visibility === 'world_readable') {
|
||||
return isUs
|
||||
? _t("You made history visible to anyone")
|
||||
: _t("%(senderName)s made history visible to anyone", {senderName: getSenderName(event)});
|
||||
} else { // shared, default
|
||||
return isUs
|
||||
? _t("You made history visible to future members")
|
||||
: _t("%(senderName)s made history visible to future members", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { TagID } from "../models";
|
||||
|
||||
/**
|
||||
* Represents an event preview.
|
||||
*/
|
||||
export interface IPreview {
|
||||
/**
|
||||
* Gets the text which represents the event as a preview.
|
||||
* @param event The event to preview.
|
||||
* @param tagId Optional. The tag where the room the event was sent in resides.
|
||||
* @returns The preview.
|
||||
*/
|
||||
getTextFor(event: MatrixEvent, tagId?: TagID): string;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getTargetName, isSelfTarget } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class MembershipEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const newMembership = event.getContent()['membership'];
|
||||
const oldMembership = event.getPrevContent()['membership'];
|
||||
const reason = event.getContent()['reason'];
|
||||
const isUs = isSelfTarget(event);
|
||||
|
||||
if (newMembership === 'invite') {
|
||||
return isUs
|
||||
? _t("You were invited")
|
||||
: _t("%(targetName)s was invited", {targetName: getTargetName(event)});
|
||||
} else if (newMembership === 'leave' && oldMembership !== 'invite') {
|
||||
if (event.getSender() === event.getStateKey()) {
|
||||
return isUs
|
||||
? _t("You left")
|
||||
: _t("%(targetName)s left", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
if (reason) {
|
||||
return isUs
|
||||
? _t("You were kicked (%(reason)s)", {reason})
|
||||
: _t("%(targetName)s was kicked (%(reason)s)", {targetName: getTargetName(event), reason});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were kicked")
|
||||
: _t("%(targetName)s was kicked", {targetName: getTargetName(event)});
|
||||
}
|
||||
}
|
||||
} else if (newMembership === 'leave' && oldMembership === 'invite') {
|
||||
if (event.getSender() === event.getStateKey()) {
|
||||
return isUs
|
||||
? _t("You rejected the invite")
|
||||
: _t("%(targetName)s rejected the invite", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were uninvited")
|
||||
: _t("%(targetName)s was uninvited", {targetName: getTargetName(event)});
|
||||
}
|
||||
} else if (newMembership === 'ban') {
|
||||
if (reason) {
|
||||
return isUs
|
||||
? _t("You were banned (%(reason)s)", {reason})
|
||||
: _t("%(targetName)s was banned (%(reason)s)", {targetName: getTargetName(event), reason});
|
||||
} else {
|
||||
return isUs
|
||||
? _t("You were banned")
|
||||
: _t("%(targetName)s was banned", {targetName: getTargetName(event)});
|
||||
}
|
||||
} else if (newMembership === 'join' && oldMembership !== 'join') {
|
||||
return isUs
|
||||
? _t("You joined")
|
||||
: _t("%(targetName)s joined", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
const isDisplayNameChange = event.getContent()['displayname'] !== event.getPrevContent()['displayname'];
|
||||
const isAvatarChange = event.getContent()['avatar_url'] !== event.getPrevContent()['avatar_url'];
|
||||
if (isDisplayNameChange) {
|
||||
return isUs
|
||||
? _t("You changed your name")
|
||||
: _t("%(targetName)s changed their name", {targetName: getTargetName(event)});
|
||||
} else if (isAvatarChange) {
|
||||
return isUs
|
||||
? _t("You changed your avatar")
|
||||
: _t("%(targetName)s changed their avatar", {targetName: getTargetName(event)});
|
||||
} else {
|
||||
return null; // no change
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import ReplyThread from "../../../components/views/elements/ReplyThread";
|
||||
|
||||
export class MessageEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
let eventContent = event.getContent();
|
||||
|
||||
if (event.isRelation("m.replace")) {
|
||||
// It's an edit, generate the preview on the new text
|
||||
eventContent = event.getContent()['m.new_content'];
|
||||
}
|
||||
|
||||
let body = (eventContent['body'] || '').trim();
|
||||
const msgtype = eventContent['msgtype'];
|
||||
if (!body || !msgtype) return null; // invalid event, no preview
|
||||
|
||||
// XXX: Newer relations have a getRelation() function which is not compatible with replies.
|
||||
const mRelatesTo = event.getWireContent()['m.relates_to'];
|
||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||
// If this is a reply, get the real reply and use that
|
||||
body = (ReplyThread.stripPlainReply(body) || '').trim();
|
||||
if (!body) return null; // invalid event, no preview
|
||||
}
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
||||
}
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return body;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(message)s", {senderName: getSenderName(event), message: body});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class NameEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You changed the room name");
|
||||
} else {
|
||||
return _t("%(senderName)s changed the room name", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class ReactionEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const reaction = event.getRelation().key;
|
||||
if (!reaction) return;
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return reaction;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(reaction)s", {senderName: getSenderName(event), reaction});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class StickerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
const stickerName = event.getContent()['body'];
|
||||
if (!stickerName) return null;
|
||||
|
||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
return stickerName;
|
||||
} else {
|
||||
return _t("%(senderName)s: %(stickerName)s", {senderName: getSenderName(event), stickerName});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
|
||||
export class ThirdPartyInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (!isValid3pidInvite(event)) {
|
||||
const targetName = event.getPrevContent().display_name || _t("Someone");
|
||||
if (isSelf(event)) {
|
||||
return _t("You uninvited %(targetName)s", {targetName});
|
||||
} else {
|
||||
return _t("%(senderName)s uninvited %(targetName)s", {senderName: getSenderName(event), targetName});
|
||||
}
|
||||
} else {
|
||||
const targetName = event.getContent().display_name;
|
||||
if (isSelf(event)) {
|
||||
return _t("You invited %(targetName)s", {targetName});
|
||||
} else {
|
||||
return _t("%(senderName)s invited %(targetName)s", {senderName: getSenderName(event), targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { IPreview } from "./IPreview";
|
||||
import { TagID } from "../models";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { getSenderName, isSelf } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class TopicEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (isSelf(event)) {
|
||||
return _t("You changed the room topic");
|
||||
} else {
|
||||
return _t("%(senderName)s changed the room topic", {senderName: getSenderName(event)});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { DefaultTagID, TagID } from "../models";
|
||||
|
||||
export function isSelf(event: MatrixEvent): boolean {
|
||||
const selfUserId = MatrixClientPeg.get().getUserId();
|
||||
if (event.getType() === 'm.room.member') {
|
||||
return event.getStateKey() === selfUserId;
|
||||
}
|
||||
return event.getSender() === selfUserId;
|
||||
}
|
||||
|
||||
export function isSelfTarget(event: MatrixEvent): boolean {
|
||||
const selfUserId = MatrixClientPeg.get().getUserId();
|
||||
return event.getStateKey() === selfUserId;
|
||||
}
|
||||
|
||||
export function shouldPrefixMessagesIn(roomId: string, tagId: TagID): boolean {
|
||||
if (tagId !== DefaultTagID.DM) return true;
|
||||
|
||||
// We don't prefix anything in 1:1s
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return true;
|
||||
return room.currentState.getJoinedMemberCount() !== 2;
|
||||
}
|
||||
|
||||
export function getSenderName(event: MatrixEvent): string {
|
||||
return event.sender ? event.sender.name : event.getSender();
|
||||
}
|
||||
|
||||
export function getTargetName(event: MatrixEvent): string {
|
||||
return event.target ? event.target.name : event.getStateKey();
|
||||
}
|
Loading…
Reference in New Issue