diff --git a/res/css/views/globals/_MatrixToolbar.scss b/res/css/views/globals/_MatrixToolbar.scss index 1109b9e264a..1791d619ae6 100644 --- a/res/css/views/globals/_MatrixToolbar.scss +++ b/res/css/views/globals/_MatrixToolbar.scss @@ -28,6 +28,12 @@ limitations under the License. margin-top: -2px; } +.mx_MatrixToolbar_info { + padding-left: 16px; + padding-right: 8px; + background-color: $info-bg-color; +} + .mx_MatrixToolbar_error { padding-left: 16px; padding-right: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index ad4630d668e..8ab338790e2 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -20,6 +20,7 @@ $focus-brightness: 200%; // red warning colour $warning-color: #ff0064; $warning-bg-color: #DF2A8B; +$info-bg-color: #2A9EDF; // groups $info-plinth-bg-color: #454545; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index dedfeaeb2f1..7d004bd8316 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -27,6 +27,7 @@ $focus-brightness: 125%; $warning-color: #ff0064; // background colour for warnings $warning-bg-color: #DF2A8B; +$info-bg-color: #2A9EDF; $mention-user-pill-bg-color: #ff0064; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 65ef69d547a..5b617feb689 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -30,10 +30,16 @@ import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; import MatrixClientPeg from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; +import RoomListStore from "../../stores/RoomListStore"; import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; +// We need to fetch each pinned message individually (if we don't already have it) +// so each pinned message may trigger a request. Limit the number per room for sanity. +// NB. this is just for server notices rather than pinned messages in general. +const MAX_PINNED_NOTICES_PER_ROOM = 2; + /** * This is what our MatrixChat shows when we are logged in. The precise view is * determined by the page_type property. @@ -80,6 +86,8 @@ const LoggedInView = React.createClass({ return { // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), + // any currently active server notice events + serverNoticeEvents: [], }; }, @@ -97,13 +105,18 @@ const LoggedInView = React.createClass({ ); this._setStateFromSessionStore(); + this._updateServerNoticeEvents(); + this._matrixClient.on("accountData", this.onAccountData); this._matrixClient.on("sync", this.onSync); + this._matrixClient.on("RoomState.events", this.onRoomStateEvents); }, componentWillUnmount: function() { document.removeEventListener('keydown', this._onKeyDown); this._matrixClient.removeListener("accountData", this.onAccountData); + this._matrixClient.removeListener("sync", this.onSync); + this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); if (this._sessionStoreToken) { this._sessionStoreToken.remove(); } @@ -157,8 +170,42 @@ const LoggedInView = React.createClass({ syncErrorData: null, }); } + + if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { + this._updateServerNoticeEvents(); + } }, + onRoomStateEvents: function(ev, state) { + const roomLists = RoomListStore.getRoomLists(); + if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { + this._updateServerNoticeEvents(); + } + }, + + _updateServerNoticeEvents: async function() { + const roomLists = RoomListStore.getRoomLists(); + if (!roomLists['m.server_notice']) return []; + + const pinnedEvents = []; + for (const room of roomLists['m.server_notice']) { + const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); + + if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; + + const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); + for (const eventId of pinnedEventIds) { + const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); + const ev = timeline.getEvents().find(ev => ev.getId() === eventId); + if (ev) pinnedEvents.push(ev); + } + } + this.setState({ + serverNoticeEvents: pinnedEvents, + }); + }, + + _onKeyDown: function(ev) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers @@ -386,10 +433,18 @@ const LoggedInView = React.createClass({ break; } + const mauLimitEvent = this.state.serverNoticeEvents.find((e) => { + return e && e.getType() === 'm.server_notice.usage_limit_reached' && + e.getContent().limit_type && + e.getContent().limit_type === 'monthly_active_user' + }); + let topBar; const isGuest = this.props.matrixClient.isGuest(); if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_MAU_LIMIT_EXCEEDED') { - topBar = ; + topBar = ; + } else if (mauLimitEvent) { + topBar = ; } else if (this.props.showCookieBar && this.props.config.piwik ) { diff --git a/src/components/views/globals/ServerLimitBar.js b/src/components/views/globals/ServerLimitBar.js index 638e85634fc..c0a7376bd6c 100644 --- a/src/components/views/globals/ServerLimitBar.js +++ b/src/components/views/globals/ServerLimitBar.js @@ -15,15 +15,63 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { _t } from '../../../languageHandler'; export default React.createClass({ + propTypes: { + // 'hard' if the logged in user has been locked out, 'soft' if they haven't + kind: PropTypes.string, + adminContent: PropTypes.string, + }, + + getDefaultProps: function() { + return { + kind: 'hard', + }; + }, + render: function() { - const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_error"; + const toolbarClasses = { + 'mx_MatrixToolbar': true, + }; + let content; + + const translateLink = (sub) => { + if (this.props.adminContent) { + return {sub}; + } else { + return sub; + } + }; + + if (this.props.kind === 'hard') { + toolbarClasses['mx_MatrixToolbar_error'] = true; + content = _t( + "This homeserver has hit its Monthly Active User limit. " + + "Please contact your service administrator to continue using the service.", + {}, + { + 'a': translateLink, + }, + ); + } else { + toolbarClasses['mx_MatrixToolbar_info'] = true; + content = _t( + "This homeserver has hit its Monthly Active User " + + "limit so some users will not be able to log in. " + + "Please contact your service administrator to get this limit increased.", + {}, + { + 'a': translateLink, + }, + ); + } return ( -
+
- { _t("This homeserver has hit its Monthly Active User limit. Please contact your service administrator to continue using the service.") } + { content }
); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2bd0f473bc2..759951d36df 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd +Copyright 2017, 2018 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. @@ -451,6 +451,8 @@ module.exports = React.createClass({ } } + if (!this.stickies) return; + const self = this; let scrollStuckOffset = 0; // Scroll to the passed in position, i.e. a header was clicked and in a scroll to state diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d44187b834c..6fe775770c6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -464,9 +464,9 @@ "People": "People", "Rooms": "Rooms", "Low priority": "Low priority", - "System Alerts": "System Alerts", "You have no historical rooms": "You have no historical rooms", "Historical": "Historical", + "System Alerts": "System Alerts", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", "You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.", @@ -678,7 +678,8 @@ "A new version of Riot is available.": "A new version of Riot is available.", "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", "Set Password": "Set Password", - "This homeserver has hit its Monthly Active User limit. Please contact your service administrator to continue using the service.": "This homeserver has hit its Monthly Active User limit. Please contact your service administrator to continue using the service.", + "This homeserver has hit its Monthly Active User limit. Please contact your service administrator to continue using the service.": "This homeserver has hit its Monthly Active User limit. Please contact your service administrator to continue using the service.", + "This homeserver has hit its Monthly Active User limit so some users will not be able to log in. Please contact your service administrator to get this limit increased.": "This homeserver has hit its Monthly Active User limit so some users will not be able to log in. Please contact your service administrator to get this limit increased.", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", "Checking for an update...": "Checking for an update...", "No update available.": "No update available.",