diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 6bea2cbb92..0edad8d4a5 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -68,10 +68,8 @@ module.exports = { const names = whoIsTyping.map(function(m) { return m.name; }); - if (othersCount==1) { - return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')}); - } else if (othersCount>1) { - return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); + if (othersCount>=1) { + return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); } else { const lastPerson = names.pop(); return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 4c7d039da4..c93ae4fb2a 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,6 +29,10 @@ export default class AutocompleteProvider { } } + destroy() { + // stub + } + /** * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. */ diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 5b10110f04..94d2ed28de 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,5 +1,6 @@ /* Copyright 2016 Aviral Dasgupta +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45,41 +46,56 @@ const PROVIDERS = [ EmojiProvider, CommandProvider, DuckDuckGoProvider, -].map((completer) => completer.getInstance()); +]; // Providers will get rejected if they take longer than this. const PROVIDER_COMPLETION_TIMEOUT = 3000; -export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { - /* Note: That this waits for all providers to return is *intentional* - otherwise, we run into a condition where new completions are displayed - while the user is interacting with the list, which makes it difficult - to predict whether an action will actually do what is intended - */ - const completionsList = await Promise.all( - // Array of inspections of promises that might timeout. Instead of allowing a - // single timeout to reject the Promise.all, reflect each one and once they've all - // settled, filter for the fulfilled ones - PROVIDERS.map((provider) => { - return provider - .getCompletions(query, selection, force) - .timeout(PROVIDER_COMPLETION_TIMEOUT) - .reflect(); - }), - ); +export default class Autocompleter { + constructor(room) { + this.room = room; + this.providers = PROVIDERS.map((p) => { + return new p(room); + }); + } - return completionsList.filter( - (inspection) => inspection.isFulfilled(), - ).map((completionsState, i) => { - return { - completions: completionsState.value(), - provider: PROVIDERS[i], + destroy() { + this.providers.forEach((p) => { + p.destroy(); + }); + } - /* the currently matched "command" the completer tried to complete - * we pass this through so that Autocomplete can figure out when to - * re-show itself once hidden. - */ - command: PROVIDERS[i].getCurrentCommand(query, selection, force), - }; - }); + async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + /* Note: This intentionally waits for all providers to return, + otherwise, we run into a condition where new completions are displayed + while the user is interacting with the list, which makes it difficult + to predict whether an action will actually do what is intended + */ + const completionsList = await Promise.all( + // Array of inspections of promises that might timeout. Instead of allowing a + // single timeout to reject the Promise.all, reflect each one and once they've all + // settled, filter for the fulfilled ones + this.providers.map((provider) => { + return provider + .getCompletions(query, selection, force) + .timeout(PROVIDER_COMPLETION_TIMEOUT) + .reflect(); + }), + ); + + return completionsList.filter( + (inspection) => inspection.isFulfilled(), + ).map((completionsState, i) => { + return { + completions: completionsState.value(), + provider: this.providers[i], + + /* the currently matched "command" the completer tried to complete + * we pass this through so that Autocomplete can figure out when to + * re-show itself once hidden. + */ + command: this.providers[i].getCurrentCommand(query, selection, force), + }; + }); + } } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index e85457e6aa..d47f1a161a 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -109,8 +110,6 @@ const COMMANDS = [ const COMMAND_RE = /(^\/\w*)/g; -let instance = null; - export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); @@ -142,12 +141,6 @@ export default class CommandProvider extends AutocompleteProvider { return '*️⃣ ' + _t('Commands'); } - static getInstance(): CommandProvider { - if (instance === null) instance = new CommandProvider(); - - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index b2e85c4668..68d4915f56 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,8 +26,6 @@ import {TextualCompletion} from './Components'; const DDG_REGEX = /\/ddg\s+(.+)$/g; const REFERRER = 'vector'; -let instance = null; - export default class DuckDuckGoProvider extends AutocompleteProvider { constructor() { super(DDG_REGEX); @@ -96,13 +95,6 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return '🔍 ' + _t('Results from DuckDuckGo'); } - static getInstance(): DuckDuckGoProvider { - if (instance == null) { - instance = new DuckDuckGoProvider(); - } - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a5b80e3b0e..9f1f40dbe7 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -70,8 +71,6 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor }; }); -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -151,11 +150,6 @@ export default class EmojiProvider extends AutocompleteProvider { return '😃 ' + _t('Emoji'); } - static getInstance() { - if (instance == null) {instance = new EmojiProvider();} - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index cc04f54dda..1e1928a1ee 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,8 +28,6 @@ import _sortBy from 'lodash/sortBy'; const ROOM_REGEX = /(?=#)(\S*)/g; -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -96,14 +95,6 @@ export default class RoomProvider extends AutocompleteProvider { return '💬 ' + _t('Rooms'); } - static getInstance() { - if (instance == null) { - instance = new RoomProvider(); - } - - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 296399c06c..8b43964b1a 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,6 +2,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -30,20 +31,55 @@ import type {Room, RoomMember} from 'matrix-js-sdk'; const USER_REGEX = /@\S*/g; -let instance = null; - export default class UserProvider extends AutocompleteProvider { users: Array = null; room: Room = null; - constructor() { + constructor(room) { super(USER_REGEX, { keys: ['name'], }); + this.room = room; this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, }); + + this._onRoomTimelineBound = this._onRoomTimeline.bind(this); + this._onRoomStateMemberBound = this._onRoomStateMember.bind(this); + + MatrixClientPeg.get().on("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMemberBound); + } + + destroy() { + MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + } + + _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + if (!room) return; + if (removed) return; + if (room.roomId !== this.room.roomId) return; + + // ignore events from filtered timelines + if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; + + // ignore anything but real-time updates at the end of the room: + // updates from pagination will happen when the paginate completes. + if (toStartOfTimeline || !data || !data.liveEvent) return; + + this.onUserSpoke(ev.sender); + } + + _onRoomStateMember(ev, state, member) { + // ignore members in other rooms + if (member.roomId !== this.room.roomId) { + return; + } + + // blow away the users cache + this.users = null; } async getCompletions(query: string, selection: {start: number, end: number}, force = false) { @@ -86,11 +122,6 @@ export default class UserProvider extends AutocompleteProvider { return '👥 ' + _t('Users'); } - setUserListFromRoom(room: Room) { - this.room = room; - this.users = null; - } - _makeUsers() { const events = this.room.getLiveTimeline().getEvents(); const lastSpoken = {}; @@ -123,13 +154,6 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - static getInstance(): UserProvider { - if (instance == null) { - instance = new UserProvider(); - } - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index b6a450fbb4..cc4783fdac 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; +import GeminiScrollbar from 'react-gemini-scrollbar'; import {MatrixClient} from 'matrix-js-sdk'; import sdk from '../../index'; import { _t, _tJsx } from '../../languageHandler'; import withMatrixClient from '../../wrappers/withMatrixClient'; import AccessibleButton from '../views/elements/AccessibleButton'; import dis from '../../dispatcher'; -import PropTypes from 'prop-types'; import Modal from '../../Modal'; import FlairStore from '../../stores/FlairStore'; @@ -115,18 +116,17 @@ export default withMatrixClient(React.createClass({ const TintableSvg = sdk.getComponent("elements.TintableSvg"); let content; + let contentHeader; if (this.state.groups) { const groupNodes = []; this.state.groups.forEach((g) => { groupNodes.push(); }); + contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? -
-

{ _t('Your Communities') }

-
- { groupNodes } -
-
: + + { groupNodes } + :
{ _t( "You're not currently a member of any communities.", @@ -176,6 +176,7 @@ export default withMatrixClient(React.createClass({
+ { contentHeader } { content }
; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9b6dbb4c27..a40fff274c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -44,8 +44,6 @@ const Rooms = require('../../Rooms'); import KeyCode from '../../KeyCode'; -import UserProvider from '../../autocomplete/UserProvider'; - import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; @@ -541,12 +539,6 @@ module.exports = React.createClass({ }); } } - - // update the tab complete list as it depends on who most recently spoke, - // and that has probably just changed - if (ev.sender) { - UserProvider.getInstance().onUserSpoke(ev.sender); - } }, onRoomName: function(room) { @@ -568,7 +560,6 @@ module.exports = React.createClass({ this._warnAboutEncryption(room); this._calculatePeekRules(room); this._updatePreviewUrlVisibility(room); - UserProvider.getInstance().setUserListFromRoom(room); }, _warnAboutEncryption: function(room) { @@ -722,9 +713,6 @@ module.exports = React.createClass({ // refresh the conf call notification state this._updateConfCallNotification(); - // refresh the tab complete list - UserProvider.getInstance().setUserListFromRoom(this.state.room); - // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking // into. diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 851d13b23e..bdcc641391 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -168,7 +168,7 @@ module.exports = React.createClass({ } else if (this.state.progress === "sent_email") { resetPasswordJsx = (
- { _t('An email has been sent to') } { this.state.email }. { _t("Once you've followed the link it contains, click below") }. + { _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.js index 4f34cc2c16..5a18213eec 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.js @@ -54,11 +54,11 @@ export default React.createClass({ // extract the props we use from props so we can pass any others through // should consider adding this as a global rule in js-sdk? /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ - const {groupId, groupAvatarUrl, ...otherProps} = this.props; + const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props; return (
diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 596838febe..de6f801a21 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -86,7 +86,6 @@ module.exports = React.createClass({ const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this._renderNameList(userNames); - const plural = userNames.length > 1; const splitTransitions = transitions.split(','); @@ -101,13 +100,13 @@ module.exports = React.createClass({ const descs = coalescedTransitions.map((t) => { return this._getDescriptionForTransition( - t.transitionType, plural, t.repeats, + t.transitionType, userNames.length, t.repeats, ); }); const desc = this._renderCommaSeparatedList(descs); - return nameList + " " + desc; + return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc }); }); if (!summaries) { @@ -208,148 +207,75 @@ module.exports = React.createClass({ * For a certain transition, t, describe what happened to the users that * underwent the transition. * @param {string} t the transition type. - * @param {boolean} plural whether there were multiple users undergoing the same - * transition. + * @param {integer} userCount number of usernames * @param {number} repeats the number of times the transition was repeated in a row. * @returns {string} the written Human Readable equivalent of the transition. */ - _getDescriptionForTransition(t, plural, repeats) { + _getDescriptionForTransition(t, userCount, repeats) { // The empty interpolations 'severalUsers' and 'oneUser' // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. let res = null; switch(t) { case "joined": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sjoined", { severalUsers: "" }) - : _t("%(oneUser)sjoined", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count: repeats }); break; case "left": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sleft", { severalUsers: "" }) - : _t("%(oneUser)sleft", { oneUser: "" }); - } - break; + res = (userCount > 1) + ? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sleft %(count)s times", { oneUser: "", count: repeats }); + break; case "joined_and_left": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sjoined and left", { severalUsers: "" }) - : _t("%(oneUser)sjoined and left", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count: repeats }); break; case "left_and_joined": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" }) - : _t("%(oneUser)sleft and rejoined", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count: repeats }); break; case "invite_reject": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)srejected their invitations", { severalUsers: "" }) - : _t("%(oneUser)srejected their invitation", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats }); break; case "invite_withdrawal": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" }) - : _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats }); break; case "invited": - if (repeats > 1) { - res = (plural) - ? _t("were invited %(repeats)s times", { repeats: repeats }) - : _t("was invited %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were invited") - : _t("was invited"); - } + res = (userCount > 1) + ? _t("were invited %(count)s times", { count: repeats }) + : _t("was invited %(count)s times", { count: repeats }); break; case "banned": - if (repeats > 1) { - res = (plural) - ? _t("were banned %(repeats)s times", { repeats: repeats }) - : _t("was banned %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were banned") - : _t("was banned"); - } + res = (userCount > 1) + ? _t("were banned %(count)s times", { count: repeats }) + : _t("was banned %(count)s times", { count: repeats }); break; case "unbanned": - if (repeats > 1) { - res = (plural) - ? _t("were unbanned %(repeats)s times", { repeats: repeats }) - : _t("was unbanned %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were unbanned") - : _t("was unbanned"); - } + res = (userCount > 1) + ? _t("were unbanned %(count)s times", { count: repeats }) + : _t("was unbanned %(count)s times", { count: repeats }); break; case "kicked": - if (repeats > 1) { - res = (plural) - ? _t("were kicked %(repeats)s times", { repeats: repeats }) - : _t("was kicked %(repeats)s times", { repeats: repeats }); - } else { - res = (plural) - ? _t("were kicked") - : _t("was kicked"); - } + res = (userCount > 1) + ? _t("were kicked %(count)s times", { count: repeats }) + : _t("was kicked %(count)s times", { count: repeats }); break; case "changed_name": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)schanged their name", { severalUsers: "" }) - : _t("%(oneUser)schanged their name", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count: repeats }); break; case "changed_avatar": - if (repeats > 1) { - res = (plural) - ? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats }) - : _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats }); - } else { - res = (plural) - ? _t("%(severalUsers)schanged their avatar", { severalUsers: "" }) - : _t("%(oneUser)schanged their avatar", { oneUser: "" }); - } + res = (userCount > 1) + ? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats }) + : _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats }); break; } @@ -376,11 +302,9 @@ module.exports = React.createClass({ return ""; } else if (items.length === 1) { return items[0]; - } else if (remaining) { + } else if (remaining > 0) { items = items.slice(0, itemLimit); - return (remaining > 1) - ? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } ) - : _t("%(items)s and one other", { items: items.join(', ') }); + return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ) } else { const lastItem = items.pop(); return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 51ae85ba5a..a85f83d78c 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -37,11 +37,20 @@ const Pill = React.createClass({ isMessagePillUrl: (url) => { return !!REGEX_LOCAL_MATRIXTO.exec(url); }, + roomNotifPos: (text) => { + return text.indexOf("@room"); + }, + roomNotifLen: () => { + return "@room".length; + }, TYPE_USER_MENTION: 'TYPE_USER_MENTION', TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION', + TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention }, props: { + // The Type of this Pill. If url is given, this is auto-detected. + type: PropTypes.string, // The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl) url: PropTypes.string, // Whether the pill is in a message @@ -72,14 +81,20 @@ const Pill = React.createClass({ regex = REGEX_LOCAL_MATRIXTO; } - // Default to the empty array if no match for simplicity - // resource and prefix will be undefined instead of throwing - const matrixToMatch = regex.exec(nextProps.url) || []; + let matrixToMatch; + let resourceId; + let prefix; - const resourceId = matrixToMatch[1]; // The room/user ID - const prefix = matrixToMatch[2]; // The first character of prefix + if (nextProps.url) { + // Default to the empty array if no match for simplicity + // resource and prefix will be undefined instead of throwing + matrixToMatch = regex.exec(nextProps.url) || []; - const pillType = { + resourceId = matrixToMatch[1]; // The room/user ID + prefix = matrixToMatch[2]; // The first character of prefix + } + + const pillType = this.props.type || { '@': Pill.TYPE_USER_MENTION, '#': Pill.TYPE_ROOM_MENTION, '!': Pill.TYPE_ROOM_MENTION, @@ -88,6 +103,10 @@ const Pill = React.createClass({ let member; let room; switch (pillType) { + case Pill.TYPE_AT_ROOM_MENTION: { + room = nextProps.room; + } + break; case Pill.TYPE_USER_MENTION: { const localMember = nextProps.room.getMember(resourceId); member = localMember; @@ -160,6 +179,17 @@ const Pill = React.createClass({ let href = this.props.url; let onClick; switch (this.state.pillType) { + case Pill.TYPE_AT_ROOM_MENTION: { + const room = this.props.room; + if (room) { + linkText = "@room"; + if (this.props.shouldShowPillAvatar) { + avatar = ; + } + pillClass = 'mx_AtRoomPill'; + } + } + break; case Pill.TYPE_USER_MENTION: { // If this user is not a member of this room, default to the empty member const member = this.state.member; diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index d7a04247ec..fcc9acb00b 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -44,21 +44,21 @@ export default React.createClass({ const label = { groupName } ; - const badge =
!
; + const badge =
!
; return ( - -
+ +
{ av }
-
+
{ label } { badge }
diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 4c0b54e891..01270cd79d 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -85,6 +85,8 @@ module.exports = React.createClass({ Modal.createDialog(ConfirmUserActionDialog, { groupMember: this.props.groupMember, action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'), + title: this.state.isUserInvited ? _t('Disinvite this user from community?') + : _t('Remove this user from community?'), danger: true, onFinished: (proceed) => { if (!proceed) return; diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js new file mode 100644 index 0000000000..3f0b0067d2 --- /dev/null +++ b/src/components/views/groups/GroupRoomInfo.js @@ -0,0 +1,242 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import PropTypes from 'prop-types'; +import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk'; +import dis from '../../../dispatcher'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import GroupStoreCache from '../../../stores/GroupStoreCache'; +import GeminiScrollbar from 'react-gemini-scrollbar'; + +module.exports = React.createClass({ + displayName: 'GroupRoomInfo', + + contextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient), + }, + + propTypes: { + groupId: PropTypes.string, + groupRoomId: PropTypes.string, + }, + + getInitialState: function() { + return { + isUserPrivilegedInGroup: null, + groupRoom: null, + groupRoomPublicityLoading: false, + groupRoomRemoveLoading: false, + }; + }, + + componentWillMount: function() { + this._initGroupStore(this.props.groupId); + }, + + componentWillReceiveProps(newProps) { + if (newProps.groupId !== this.props.groupId) { + this._unregisterGroupStore(); + this._initGroupStore(newProps.groupId); + } + }, + + componentWillUnmount() { + this._unregisterGroupStore(); + }, + + _initGroupStore(groupId) { + this._groupStore = GroupStoreCache.getGroupStore( + this.context.matrixClient, this.props.groupId, + ); + this._groupStore.registerListener(this.onGroupStoreUpdated); + }, + + _unregisterGroupStore() { + if (this._groupStore) { + this._groupStore.unregisterListener(this.onGroupStoreUpdated); + } + }, + + _updateGroupRoom() { + this.setState({ + groupRoom: this._groupStore.getGroupRooms().find( + (r) => r.roomId === this.props.groupRoomId, + ), + }); + }, + + onGroupStoreUpdated: function() { + this.setState({ + isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(), + }); + this._updateGroupRoom(); + }, + + _onRemove: function(e) { + const groupId = this.props.groupId; + const roomName = this.state.groupRoom.displayname; + e.preventDefault(); + e.stopPropagation(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, { + title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}), + description: _t("Removing a room from the community will also remove it from the community page."), + button: _t("Remove"), + onFinished: (proceed) => { + if (!proceed) return; + this.setState({groupRoomRemoveLoading: true}); + const groupId = this.props.groupId; + const roomId = this.props.groupRoomId; + this._groupStore.removeRoomFromGroup(roomId).then(() => { + dis.dispatch({ + action: "view_group_room_list", + }); + }).catch((err) => { + console.error(`Error whilst removing ${roomId} from ${groupId}`, err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { + title: _t("Failed to remove room from community"), + description: _t( + "Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}, + ), + }); + }).finally(() => { + this.setState({groupRoomRemoveLoading: false}); + }); + }, + }); + }, + + _onCancel: function(e) { + dis.dispatch({ + action: "view_group_room_list", + }); + }, + + _changeGroupRoomPublicity(e) { + const isPublic = e.target.value === "public"; + this.setState({ + groupRoomPublicityLoading: true, + }); + const groupId = this.props.groupId; + const roomId = this.props.groupRoomId; + const roomName = this.state.groupRoom.displayname; + this._groupStore.updateGroupRoomAssociation(roomId, isPublic).catch((err) => { + console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { + title: _t("Something went wrong!"), + description: _t( + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.", + {roomName, groupId}, + ), + }); + }).finally(() => { + this.setState({ + groupRoomPublicityLoading: false, + }); + }); + }, + + render: function() { + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const EmojiText = sdk.getComponent('elements.EmojiText'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) { + const Spinner = sdk.getComponent("elements.Spinner"); + return
+ +
; + } + + let adminTools; + if (this.state.isUserPrivilegedInGroup) { + adminTools = +
+

{ _t("Admin Tools") }

+
+ + { _t('Remove from community') } + +
+

+ { _t('Visibility in Room List') } + { this.state.groupRoomPublicityLoading ? + :
+ } +

+
+ +
+
+ +
+
; + } + + const avatarUrl = this.context.matrixClient.mxcUrlToHttp( + this.state.groupRoom.avatarUrl, + 36, 36, 'crop', + ); + + const groupRoomName = this.state.groupRoom.displayname; + const avatar = ; + return ( +
+ + + + +
+ { avatar } +
+ + { groupRoomName } + +
+
+ { this.state.groupRoom.canonical_alias } +
+
+ + { adminTools } +
+
+ ); + }, +}); diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 94dc8e593f..907ce93a4a 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -16,13 +16,10 @@ limitations under the License. import React from 'react'; import {MatrixClient} from 'matrix-js-sdk'; -import { _t } from '../../../languageHandler'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupRoomType } from '../../../groups'; -import GroupStoreCache from '../../../stores/GroupStoreCache'; -import Modal from '../../../Modal'; const GroupRoomTile = React.createClass({ displayName: 'GroupRoomTile', @@ -32,68 +29,11 @@ const GroupRoomTile = React.createClass({ groupRoom: GroupRoomType.isRequired, }, - getInitialState: function() { - return { - name: this.calculateRoomName(this.props.groupRoom), - }; - }, - - componentWillReceiveProps: function(newProps) { - this.setState({ - name: this.calculateRoomName(newProps.groupRoom), - }); - }, - - calculateRoomName: function(groupRoom) { - return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room"); - }, - - removeRoomFromGroup: function() { - const groupId = this.props.groupId; - const groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); - const roomName = this.state.name; - const roomId = this.props.groupRoom.roomId; - groupStore.removeRoomFromGroup(roomId) - .catch((err) => { - console.error(`Error whilst removing ${roomId} from ${groupId}`, err); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { - title: _t("Failed to remove room from community"), - description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}), - }); - }); - }, - onClick: function(e) { - let roomId; - let roomAlias; - if (this.props.groupRoom.canonicalAlias) { - roomAlias = this.props.groupRoom.canonicalAlias; - } else { - roomId = this.props.groupRoom.roomId; - } dis.dispatch({ - action: 'view_room', - room_id: roomId, - room_alias: roomAlias, - }); - }, - - onDeleteClick: function(e) { - const groupId = this.props.groupId; - const roomName = this.state.name; - e.preventDefault(); - e.stopPropagation(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, { - title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}), - description: _t("Removing a room from the community will also remove it from the community page."), - button: _t("Remove"), - onFinished: (success) => { - if (success) { - this.removeRoomFromGroup(); - } - }, + action: 'view_group_room', + groupId: this.props.groupId, + groupRoomId: this.props.groupRoom.roomId, }); }, @@ -106,7 +46,7 @@ const GroupRoomTile = React.createClass({ ); const av = ( - @@ -118,14 +58,8 @@ const GroupRoomTile = React.createClass({ { av }
- { this.state.name } + { this.props.groupRoom.displayname }
- - -
); }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 4c53c23f76..5f5a74ccd1 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -20,7 +20,7 @@ import url from 'url'; import classnames from 'classnames'; import sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import { _t, _tJsx } from '../../../languageHandler'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -256,7 +256,7 @@ export const EmailIdentityAuthEntry = React.createClass({ } else { return (
-

{ _t("An email has been sent to") } { this.props.inputs.emailAddress }

+

{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => {this.props.inputs.emailAddress}) }

{ _t("Please check your email to continue registration.") }

); @@ -370,7 +370,7 @@ export const MsisdnAuthEntry = React.createClass({ }); return (
-

{ _t("A text message has been sent to") } +{ this._msisdn }

+

{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => {this._msisdn}) }

{ _t("Please enter the code it contains:") }

diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 63e3144115..afdb97272f 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -19,6 +19,7 @@ import React from 'react'; import sdk from '../../../index'; import Flair from '../elements/Flair.js'; +import { _tJsx } from '../../../languageHandler'; export default function SenderProfile(props) { const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -30,23 +31,39 @@ export default function SenderProfile(props) { return ; // emote message must include the name so don't duplicate it } + // Name + flair + const nameElem = [ + { name || '' }, + props.enableFlair ? + + : null, + ]; + + let content = ''; + + if(props.text) { + // Replace senderName, and wrap surrounding text in spans with the right class + content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [ + p1 ? { p1 } : null, + nameElem, + p2 ? { p2 } : null, + ]); + } else { + content = nameElem; + } + return (
- { name || '' } - { props.enableFlair ? - - : null - } - { props.aux ? { props.aux } : null } + { content }
); } SenderProfile.propTypes = { mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing - aux: React.PropTypes.string, // stuff to go after the sender name, if anything + text: React.PropTypes.string, // Text to show. Defaults to sender name onClick: React.PropTypes.func, }; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 64b23238e5..911f2c98d1 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -34,6 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import ContextualMenu from '../../structures/ContextualMenu'; import {RoomMember} from 'matrix-js-sdk'; import classNames from 'classnames'; +import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; linkifyMatrix(linkify); @@ -169,8 +170,10 @@ module.exports = React.createClass({ pillifyLinks: function(nodes) { const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; + let node = nodes[0]; + while (node) { + let pillified = false; + if (node.tagName === "A" && node.getAttribute("href")) { const href = node.getAttribute("href"); @@ -189,10 +192,71 @@ module.exports = React.createClass({ ReactDOM.render(pill, pillContainer); node.parentNode.replaceChild(pillContainer, node); + // Pills within pills aren't going to go well, so move on + pillified = true; + + // update the current node with one that's now taken its place + node = pillContainer; + } + } else if (node.nodeType == Node.TEXT_NODE) { + const Pill = sdk.getComponent('elements.Pill'); + + let currentTextNode = node; + const roomNotifTextNodes = []; + + // Take a textNode and break it up to make all the instances of @room their + // own textNode, adding those nodes to roomNotifTextNodes + while (currentTextNode !== null) { + const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent); + let nextTextNode = null; + if (roomNotifPos > -1) { + let roomTextNode = currentTextNode; + + if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos); + if (roomTextNode.textContent.length > Pill.roomNotifLen()) { + nextTextNode = roomTextNode.splitText(Pill.roomNotifLen()); + } + roomNotifTextNodes.push(roomTextNode); + } + currentTextNode = nextTextNode; + } + + if (roomNotifTextNodes.length > 0) { + const pushProcessor = new PushProcessor(MatrixClientPeg.get()); + const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); + if (pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) { + // Now replace all those nodes with Pills + for (const roomNotifTextNode of roomNotifTextNodes) { + const pillContainer = document.createElement('span'); + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const pill = ; + + ReactDOM.render(pill, pillContainer); + roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); + + // Set the next node to be processed to the one after the node + // we're adding now, since we've just inserted nodes into the structure + // we're iterating over. + // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once + node = roomNotifTextNode.nextSibling; + } + // Nothing else to do for a text node (and we don't need to advance + // the loop pointer because we did it above) + continue; + } } - } else if (node.children && node.children.length) { - this.pillifyLinks(node.children); } + + if (node.childNodes && node.childNodes.length && !pillified) { + this.pillifyLinks(node.childNodes); + } + + node = node.nextSibling; } }, diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ecc908a02c..958d16073c 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,5 +1,23 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import React from 'react'; import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; import isEqual from 'lodash/isEqual'; @@ -7,8 +25,9 @@ import sdk from '../../../index'; import type {Completion} from '../../../autocomplete/Autocompleter'; import Promise from 'bluebird'; import UserSettingsStore from '../../../UserSettingsStore'; +import { Room } from 'matrix-js-sdk'; -import {getCompletions} from '../../../autocomplete/Autocompleter'; +import Autocompleter from '../../../autocomplete/Autocompleter'; const COMPOSER_SELECTED = 0; @@ -17,6 +36,7 @@ export default class Autocomplete extends React.Component { constructor(props) { super(props); + this.autocompleter = new Autocompleter(props.room); this.completionPromise = null; this.hide = this.hide.bind(this); this.onCompletionClicked = this.onCompletionClicked.bind(this); @@ -41,6 +61,11 @@ export default class Autocomplete extends React.Component { } componentWillReceiveProps(newProps, state) { + if (this.props.room.roomId !== newProps.room.roomId) { + this.autocompleter.destroy(); + this.autocompleter = new Autocompleter(newProps.room); + } + // Query hasn't changed so don't try to complete it if (newProps.query === this.props.query) { return; @@ -49,6 +74,10 @@ export default class Autocomplete extends React.Component { this.complete(newProps.query, newProps.selection); } + componentWillUnmount() { + this.autocompleter.destroy(); + } + complete(query, selection) { this.queryRequested = query; if (this.debounceCompletionsRequest) { @@ -83,7 +112,7 @@ export default class Autocomplete extends React.Component { } processQuery(query, selection) { - return getCompletions( + return this.autocompleter.getCompletions( query, selection, this.state.forceComplete, ).then((completions) => { // Only ever process the completions for the most recent query being processed @@ -267,8 +296,11 @@ export default class Autocomplete extends React.Component { Autocomplete.propTypes = { // the query string for which to show autocomplete suggestions - query: React.PropTypes.string.isRequired, + query: PropTypes.string.isRequired, // method invoked with range and text content when completion is confirmed - onConfirm: React.PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + + // The room in which we're autocompleting + room: PropTypes.instanceOf(Room), }; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 499d0ec09a..812d72a26a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -19,7 +19,7 @@ limitations under the License. const React = require('react'); const classNames = require("classnames"); -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; const Modal = require('../../../Modal'); const sdk = require('../../../index'); @@ -502,12 +502,12 @@ module.exports = withMatrixClient(React.createClass({ } if (needsSenderProfile) { - let aux = null; + let text = null; if (!this.props.tileShape) { - if (msgtype === 'm.image') aux = _t('sent an image'); - else if (msgtype === 'm.video') aux = _t('sent a video'); - else if (msgtype === 'm.file') aux = _t('uploaded a file'); - sender = ; + if (msgtype === 'm.image') text = _td('%(senderName)s sent an image'); + else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video'); + else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); + sender = ; } else { sender = ; } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 3abff39652..c043b3714d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -256,11 +256,11 @@ module.exports = withMatrixClient(React.createClass({ onKick: function() { const membership = this.props.member.membership; - const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, { member: this.props.member, - action: kickLabel, + action: membership === "invite" ? _t("Disinvite") : _t("Kick"), + title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), askReason: membership === "join", danger: true, onFinished: (proceed, reason) => { @@ -294,6 +294,7 @@ module.exports = withMatrixClient(React.createClass({ Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, { member: this.props.member, action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"), + title: this.props.member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"), askReason: this.props.member.membership !== 'ban', danger: this.props.member.membership !== 'ban', onFinished: (proceed, reason) => { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4850428621..43f3aa5d88 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1130,10 +1131,12 @@ export default class MessageComposerInput extends React.Component {
this.autocomplete = e} + room={this.props.room} onConfirm={this.setDisplayedCompletion} onSelectionChange={this.setDisplayedCompletion} query={this.getAutocompleteQuery(content)} - selection={selection} /> + selection={selection} + />
+ + ); } else if (kicked || banned) { - const roomName = this._roomNameElement(_t('This room')); + const roomName = this._roomNameElement(); const kickerMember = this.props.room.currentState.getMember( myMember.events.member.getSender(), ); @@ -167,9 +165,17 @@ module.exports = React.createClass({ let actionText; if (kicked) { - actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + if(roomName) { + actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + } else { + actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName}); + } } else if (banned) { - actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + if(roomName) { + actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); + } else { + actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName}); + } } // no other options possible due to the kicked || banned check above. joinBlock = ( @@ -203,7 +209,7 @@ module.exports = React.createClass({ joinBlock = (
- { _t('You are trying to access %(roomName)s.', {roomName: name}) } + { name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
{ _tJsx("Click here to join the discussion!", /(.*?)<\/a>/, diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index dbdcdf596a..2f46a9308e 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -71,6 +71,7 @@ const BannedUser = React.createClass({ Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, { member: this.props.member, action: _t('Unban'), + title: _t('Unban this user?'), danger: false, onFinished: (proceed) => { if (!proceed) return; @@ -866,21 +867,21 @@ module.exports = React.createClass({ disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)} checked={historyVisibility === "shared"} onChange={this._onHistoryRadioToggle} /> - { _t('Members only') } ({ _t('since the point in time of selecting this option') }) + { _t('Members only (since the point in time of selecting this option)') }
diff --git a/src/groups.js b/src/groups.js index 06db5d067f..6c266e0fb6 100644 --- a/src/groups.js +++ b/src/groups.js @@ -15,6 +15,7 @@ limitations under the License. */ import PropTypes from 'prop-types'; +import { _t } from './languageHandler.js'; export const GroupMemberType = PropTypes.shape({ userId: PropTypes.string.isRequired, @@ -23,6 +24,7 @@ export const GroupMemberType = PropTypes.shape({ }); export const GroupRoomType = PropTypes.shape({ + displayname: PropTypes.string, name: PropTypes.string, roomId: PropTypes.string.isRequired, canonicalAlias: PropTypes.string, @@ -39,6 +41,7 @@ export function groupMemberFromApiObject(apiObject) { export function groupRoomFromApiObject(apiObject) { return { + displayname: apiObject.name || apiObject.canonical_alias || _t("Unnamed Room"), name: apiObject.name, roomId: apiObject.room_id, canonicalAlias: apiObject.canonical_alias, @@ -47,5 +50,6 @@ export function groupRoomFromApiObject(apiObject) { numJoinedMembers: apiObject.num_joined_members, worldReadable: apiObject.world_readable, guestCanJoin: apiObject.guest_can_join, + isPublic: apiObject.is_public !== false, }; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e486ebe72..f41d5add54 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -153,11 +153,12 @@ "Communities": "Communities", "Message Pinning": "Message Pinning", "%(displayName)s is typing": "%(displayName)s is typing", - "%(names)s and one other are typing": "%(names)s and one other are typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", + "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "Failure to create room": "Failure to create room", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Unnamed Room": "Unnamed Room", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -210,9 +211,9 @@ " (unsupported)": " (unsupported)", "Join as voice or video.": "Join as voice or video.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", - "sent an image": "sent an image", - "sent a video": "sent a video", - "uploaded a file": "uploaded a file", + "%(senderName)s sent an image": "%(senderName)s sent an image", + "%(senderName)s sent a video": "%(senderName)s sent a video", + "%(senderName)s uploaded a file": "%(senderName)s uploaded a file", "Options": "Options", "Undecryptable": "Undecryptable", "Encrypted by a verified device": "Encrypted by a verified device", @@ -225,9 +226,13 @@ "device id: ": "device id: ", "Disinvite": "Disinvite", "Kick": "Kick", + "Disinvite this user?": "Disinvite this user?", + "Kick this user?": "Kick this user?", "Failed to kick": "Failed to kick", "Unban": "Unban", "Ban": "Ban", + "Unban this user?": "Unban this user?", + "Ban this user?": "Ban this user?", "Failed to ban user": "Failed to ban user", "Failed to mute user": "Failed to mute user", "Failed to toggle moderator status": "Failed to toggle moderator status", @@ -314,35 +319,36 @@ "Forget room": "Forget room", "Search": "Search", "Show panel": "Show panel", - "to favourite": "to favourite", - "to tag direct chat": "to tag direct chat", - "to restore": "to restore", - "to demote": "to demote", + "Drop here to favourite": "Drop here to favourite", + "Drop here to tag direct chat": "Drop here to tag direct chat", + "Drop here to restore": "Drop here to restore", + "Drop here to demote": "Drop here to demote", "Drop here to tag %(section)s": "Drop here to tag %(section)s", - "Drop here %(toAction)s": "Drop here %(toAction)s", "Press to start a chat with someone": "Press to start a chat with someone", "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", + "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", "People": "People", "Rooms": "Rooms", "Low priority": "Low priority", "Historical": "Historical", - "Unnamed Room": "Unnamed Room", - "a room": "a room", "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.", "You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s", "Would you like to accept or decline this invitation?": "Would you like to accept or decline this invitation?", - "This room": "This room", "Reason: %(reasonText)s": "Reason: %(reasonText)s", "Rejoin": "Rejoin", "You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.", + "You have been kicked from this room by %(userName)s.": "You have been kicked from this room by %(userName)s.", "You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.", + "You have been banned from this room by %(userName)s.": "You have been banned from this room by %(userName)s.", + "This room": "This room", "%(roomName)s does not exist.": "%(roomName)s does not exist.", "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", "You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.", + "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", @@ -387,10 +393,9 @@ "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "Who can read history?": "Who can read history?", "Anyone": "Anyone", - "Members only": "Members only", - "since the point in time of selecting this option": "since the point in time of selecting this option", - "since they were invited": "since they were invited", - "since they joined": "since they joined", + "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)", + "Members only (since they were invited)": "Members only (since they were invited)", + "Members only (since they joined)": "Members only (since they joined)", "Room Colour": "Room Colour", "Permissions": "Permissions", "The default role for new room members is": "The default role for new room members is", @@ -463,10 +468,10 @@ "Dismiss": "Dismiss", "To continue, please enter your password.": "To continue, please enter your password.", "Password:": "Password:", - "An email has been sent to": "An email has been sent to", + "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s", "Please check your email to continue registration.": "Please check your email to continue registration.", "Token incorrect": "Token incorrect", - "A text message has been sent to": "A text message has been sent to", + "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "Please enter the code it contains:": "Please enter the code it contains:", "Start authentication": "Start authentication", "powered by Matrix": "powered by Matrix", @@ -489,16 +494,22 @@ "Identity server URL": "Identity server URL", "What does this mean?": "What does this mean?", "Remove from community": "Remove from community", + "Disinvite this user from community?": "Disinvite this user from community?", + "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "Filter community members": "Filter community members", - "Filter community rooms": "Filter community rooms", - "Failed to remove room from community": "Failed to remove room from community", - "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Remove": "Remove", - "Remove this room from the community": "Remove this room from the community", + "Failed to remove room from community": "Failed to remove room from community", + "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", + "Something went wrong!": "Something went wrong!", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "The visibility of '%(roomName)s' in %(groupId)s could not be updated.", + "Visibility in Room List": "Visibility in Room List", + "Visible to everyone": "Visible to everyone", + "Only visible to community members": "Only visible to community members", + "Filter community rooms": "Filter community rooms", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Do you want to load widget from URL:": "Do you want to load widget from URL:", @@ -518,56 +529,57 @@ "Integrations Error": "Integrations Error", "Could not connect to the integration server": "Could not connect to the integration server", "Manage Integrations": "Manage Integrations", - "%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times", - "%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times", - "%(severalUsers)sjoined": "%(severalUsers)sjoined", - "%(oneUser)sjoined": "%(oneUser)sjoined", - "%(severalUsers)sleft %(repeats)s times": "%(severalUsers)sleft %(repeats)s times", - "%(oneUser)sleft %(repeats)s times": "%(oneUser)sleft %(repeats)s times", - "%(severalUsers)sleft": "%(severalUsers)sleft", - "%(oneUser)sleft": "%(oneUser)sleft", - "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)sjoined and left %(repeats)s times", - "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)sjoined and left %(repeats)s times", - "%(severalUsers)sjoined and left": "%(severalUsers)sjoined and left", - "%(oneUser)sjoined and left": "%(oneUser)sjoined and left", - "%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)sleft and rejoined %(repeats)s times", - "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)sleft and rejoined %(repeats)s times", - "%(severalUsers)sleft and rejoined": "%(severalUsers)sleft and rejoined", - "%(oneUser)sleft and rejoined": "%(oneUser)sleft and rejoined", - "%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)srejected their invitations %(repeats)s times", - "%(oneUser)srejected their invitation %(repeats)s times": "%(oneUser)srejected their invitation %(repeats)s times", - "%(severalUsers)srejected their invitations": "%(severalUsers)srejected their invitations", - "%(oneUser)srejected their invitation": "%(oneUser)srejected their invitation", - "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)shad their invitations withdrawn %(repeats)s times", - "%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)shad their invitation withdrawn %(repeats)s times", - "%(severalUsers)shad their invitations withdrawn": "%(severalUsers)shad their invitations withdrawn", - "%(oneUser)shad their invitation withdrawn": "%(oneUser)shad their invitation withdrawn", - "were invited %(repeats)s times": "were invited %(repeats)s times", - "was invited %(repeats)s times": "was invited %(repeats)s times", - "were invited": "were invited", - "was invited": "was invited", - "were banned %(repeats)s times": "were banned %(repeats)s times", - "was banned %(repeats)s times": "was banned %(repeats)s times", - "were banned": "were banned", - "was banned": "was banned", - "were unbanned %(repeats)s times": "were unbanned %(repeats)s times", - "was unbanned %(repeats)s times": "was unbanned %(repeats)s times", - "were unbanned": "were unbanned", - "was unbanned": "was unbanned", - "were kicked %(repeats)s times": "were kicked %(repeats)s times", - "was kicked %(repeats)s times": "was kicked %(repeats)s times", - "were kicked": "were kicked", - "was kicked": "was kicked", - "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)schanged their name %(repeats)s times", - "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)schanged their name %(repeats)s times", - "%(severalUsers)schanged their name": "%(severalUsers)schanged their name", - "%(oneUser)schanged their name": "%(oneUser)schanged their name", - "%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)schanged their avatar %(repeats)s times", - "%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)schanged their avatar %(repeats)s times", - "%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar", - "%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar", - "%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others", - "%(items)s and one other": "%(items)s and one other", + "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", + "were invited %(count)s times|other": "were invited %(count)s times", + "were invited %(count)s times|one": "were invited", + "was invited %(count)s times|other": "was invited %(count)s times", + "was invited %(count)s times|one": "was invited", + "were banned %(count)s times|other": "were banned %(count)s times", + "were banned %(count)s times|one": "were banned", + "was banned %(count)s times|other": "was banned %(count)s times", + "was banned %(count)s times|one": "was banned", + "were unbanned %(count)s times|other": "were unbanned %(count)s times", + "were unbanned %(count)s times|one": "were unbanned", + "was unbanned %(count)s times|other": "was unbanned %(count)s times", + "was unbanned %(count)s times|one": "was unbanned", + "were kicked %(count)s times|other": "were kicked %(count)s times", + "were kicked %(count)s times|one": "were kicked", + "was kicked %(count)s times|other": "was kicked %(count)s times", + "was kicked %(count)s times|one": "was kicked", + "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", + "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "Custom level": "Custom level", "Room directory": "Room directory", @@ -575,7 +587,6 @@ "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", - "Something went wrong!": "Something went wrong!", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", @@ -589,7 +600,6 @@ "Start Chatting": "Start Chatting", "Confirm Removal": "Confirm Removal", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", - "%(actionVerb)s this person?": "%(actionVerb)s this person?", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'", "Something went wrong whilst creating your community": "Something went wrong whilst creating your community", "Create Community": "Create Community", @@ -833,7 +843,7 @@ "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "Once you've followed the link it contains, click below": "Once you've followed the link it contains, click below", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", "I have verified my email address": "I have verified my email address", "Your password has been reset": "Your password has been reset", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device", diff --git a/src/languageHandler.js b/src/languageHandler.js index a90b78c40e..da62bfee56 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -252,6 +252,26 @@ function getLangsJson() { }); } +function weblateToCounterpart(inTrs) { + const outTrs = {}; + + for (const key of Object.keys(inTrs)) { + const keyParts = key.split('|', 2); + if (keyParts.length === 2) { + let obj = outTrs[keyParts[0]]; + if (obj === undefined) { + obj = {}; + outTrs[keyParts[0]] = obj; + } + obj[keyParts[1]] = inTrs[key]; + } else { + outTrs[key] = inTrs[key]; + } + } + + return outTrs; +} + function getLanguage(langPath) { return new Promise((resolve, reject) => { request( @@ -261,7 +281,7 @@ function getLanguage(langPath) { reject({err: err, response: response}); return; } - resolve(JSON.parse(body)); + resolve(weblateToCounterpart(JSON.parse(body))); }, ); }); diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 1ac518a4f6..9424503390 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -66,7 +66,7 @@ class FlairStore extends EventEmitter { } // Bulk lookup ongoing, return promise to resolve/reject - if (this._usersPending[userId]) { + if (this._usersPending[userId] || this._usersInFlight[userId]) { return this._usersPending[userId].prom; } @@ -91,7 +91,7 @@ class FlairStore extends EventEmitter { console.error('Could not get groups for user', this.props.userId, err); throw err; }).finally(() => { - delete this._usersPending[userId]; + delete this._usersInFlight[userId]; }); // This debounce will allow consecutive requests for the public groups of users that @@ -113,23 +113,25 @@ class FlairStore extends EventEmitter { } async _batchedGetPublicGroups(matrixClient) { - // Take the userIds from the keys of this._usersPending - const usersInFlight = Object.keys(this._usersPending); + // Move users pending to users in flight + this._usersInFlight = this._usersPending; + this._usersPending = {}; + let resp = { users: [], }; try { - resp = await matrixClient.getPublicisedGroups(usersInFlight); + resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight)); } catch (err) { // Propagate the same error to all usersInFlight - usersInFlight.forEach((userId) => { - this._usersPending[userId].reject(err); + Object.keys(this._usersInFlight).forEach((userId) => { + this._usersInFlight[userId].reject(err); }); return; } const updatedUserGroups = resp.users; - usersInFlight.forEach((userId) => { - this._usersPending[userId].resolve(updatedUserGroups[userId] || []); + Object.keys(this._usersInFlight).forEach((userId) => { + this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []); }); } diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 3afac3c049..2578d373a7 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -141,9 +141,15 @@ export default class GroupStore extends EventEmitter { return this._summary.user ? this._summary.user.is_privileged : null; } - addRoomToGroup(roomId) { + addRoomToGroup(roomId, isPublic) { return this._matrixClient - .addRoomToGroup(this.groupId, roomId) + .addRoomToGroup(this.groupId, roomId, isPublic) + .then(this._fetchRooms.bind(this)); + } + + updateGroupRoomAssociation(roomId, isPublic) { + return this._matrixClient + .updateGroupRoomAssociation(this.groupId, roomId, isPublic) .then(this._fetchRooms.bind(this)); }