mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 02:35:48 +03:00
Merge branch 'develop' into travis/granular-settings
This commit is contained in:
commit
893c39bfbe
40 changed files with 954 additions and 548 deletions
|
@ -19,7 +19,7 @@ import PlatformPeg from './PlatformPeg';
|
|||
import SdkConfig from './SdkConfig';
|
||||
|
||||
function getRedactedUrl() {
|
||||
const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
|
||||
const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
||||
// hardcoded url to make piwik happy
|
||||
return 'https://riot.im/app/' + redactedHash;
|
||||
}
|
||||
|
|
77
src/Login.js
77
src/Login.js
|
@ -143,6 +143,50 @@ export default class Login {
|
|||
Object.assign(loginParams, legacyParams);
|
||||
|
||||
const client = this._createTemporaryClient();
|
||||
|
||||
const tryFallbackHs = (originalError) => {
|
||||
const fbClient = Matrix.createClient({
|
||||
baseUrl: self._fallbackHsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}).catch((fallback_error) => {
|
||||
console.log("fallback HS login failed", fallback_error);
|
||||
// throw the original error
|
||||
throw originalError;
|
||||
});
|
||||
};
|
||||
const tryLowercaseUsername = (originalError) => {
|
||||
const loginParamsLowercase = Object.assign({}, loginParams, {
|
||||
user: username.toLowerCase(),
|
||||
identifier: {
|
||||
user: username.toLowerCase(),
|
||||
},
|
||||
});
|
||||
return client.login('m.login.password', loginParamsLowercase).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._hsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}).catch((fallback_error) => {
|
||||
console.log("Lowercase username login failed", fallback_error);
|
||||
// throw the original error
|
||||
throw originalError;
|
||||
});
|
||||
};
|
||||
|
||||
let originalLoginError = null;
|
||||
return client.login('m.login.password', loginParams).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._hsUrl,
|
||||
|
@ -151,28 +195,25 @@ export default class Login {
|
|||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}, function(error) {
|
||||
}).catch((error) => {
|
||||
originalLoginError = error;
|
||||
if (error.httpStatus === 403) {
|
||||
if (self._fallbackHsUrl) {
|
||||
const fbClient = Matrix.createClient({
|
||||
baseUrl: self._fallbackHsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}, function(fallback_error) {
|
||||
// throw the original error
|
||||
throw error;
|
||||
});
|
||||
return tryFallbackHs(originalLoginError);
|
||||
}
|
||||
}
|
||||
throw originalLoginError;
|
||||
}).catch((error) => {
|
||||
if (
|
||||
error.httpStatus === 403 &&
|
||||
loginParams.identifier.type === 'm.id.user' &&
|
||||
username.search(/[A-Z]/) > -1
|
||||
) {
|
||||
return tryLowercaseUsername(originalLoginError);
|
||||
}
|
||||
throw originalLoginError;
|
||||
}).catch((error) => {
|
||||
console.log("Login failed", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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<Completion> {
|
||||
/* 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<Completion> {
|
||||
/* 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),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <div className="mx_Autocomplete_Completion_container_block">
|
||||
{ completions }
|
||||
|
|
|
@ -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 <div className="mx_Autocomplete_Completion_container_block">
|
||||
{ completions }
|
||||
|
|
|
@ -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 <div className="mx_Autocomplete_Completion_container_pill">
|
||||
{ completions }
|
||||
|
|
|
@ -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 <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{ completions }
|
||||
|
|
|
@ -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<RoomMember> = 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 <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{ completions }
|
||||
|
|
|
@ -407,6 +407,10 @@ export default React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
summary: null,
|
||||
isGroupPublicised: null,
|
||||
isUserPrivileged: null,
|
||||
groupRooms: null,
|
||||
groupRoomsLoading: null,
|
||||
error: null,
|
||||
editing: false,
|
||||
saving: false,
|
||||
|
@ -458,8 +462,14 @@ export default React.createClass({
|
|||
}
|
||||
this.setState({
|
||||
summary,
|
||||
summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
|
||||
isGroupPublicised: this._groupStore.getGroupPublicity(),
|
||||
isUserPrivileged: this._groupStore.isUserPrivileged(),
|
||||
groupRooms: this._groupStore.getGroupRooms(),
|
||||
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
|
||||
isUserMember: this._groupStore.getGroupMembers().some(
|
||||
(m) => m.userId === MatrixClientPeg.get().credentials.userId,
|
||||
),
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
@ -650,6 +660,7 @@ export default React.createClass({
|
|||
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
const addRoomRow = this.state.editing ?
|
||||
(<AccessibleButton className="mx_GroupView_rooms_header_addRow"
|
||||
|
@ -667,7 +678,10 @@ export default React.createClass({
|
|||
<h3>{ _t('Rooms') }</h3>
|
||||
{ addRoomRow }
|
||||
</div>
|
||||
<RoomDetailList rooms={this._groupStore.getGroupRooms()} />
|
||||
{ this.state.groupRoomsLoading ?
|
||||
<Spinner /> :
|
||||
<RoomDetailList rooms={this.state.groupRooms} />
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
@ -863,7 +877,7 @@ export default React.createClass({
|
|||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
if (this.state.summary === null && this.state.error === null || this.state.saving) {
|
||||
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.summary) {
|
||||
const summary = this.state.summary;
|
||||
|
@ -884,6 +898,7 @@ export default React.createClass({
|
|||
} else {
|
||||
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
||||
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
||||
groupName={this.state.profileForm.name}
|
||||
groupAvatarUrl={this.state.profileForm.avatar_url}
|
||||
width={48} height={48} resizeMethod='crop'
|
||||
/>;
|
||||
|
@ -927,25 +942,28 @@ export default React.createClass({
|
|||
tabIndex="2"
|
||||
dir="auto" />;
|
||||
} else {
|
||||
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
|
||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||
const groupName = summary.profile ? summary.profile.name : null;
|
||||
avatarNode = <GroupAvatar
|
||||
groupId={this.props.groupId}
|
||||
groupAvatarUrl={groupAvatarUrl}
|
||||
onClick={this._onEditClick}
|
||||
groupName={groupName}
|
||||
onClick={onGroupHeaderItemClick}
|
||||
width={48} height={48}
|
||||
/>;
|
||||
if (summary.profile && summary.profile.name) {
|
||||
nameNode = <div onClick={this._onEditClick}>
|
||||
nameNode = <div onClick={onGroupHeaderItemClick}>
|
||||
<span>{ summary.profile.name }</span>
|
||||
<span className="mx_GroupView_header_groupid">
|
||||
({ this.props.groupId })
|
||||
</span>
|
||||
</div>;
|
||||
} else {
|
||||
nameNode = <span onClick={this._onEditClick}>{ this.props.groupId }</span>;
|
||||
nameNode = <span onClick={onGroupHeaderItemClick}>{ this.props.groupId }</span>;
|
||||
}
|
||||
if (summary.profile && summary.profile.short_description) {
|
||||
shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>;
|
||||
shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
|
||||
}
|
||||
}
|
||||
if (this.state.editing) {
|
||||
|
@ -986,6 +1004,7 @@ export default React.createClass({
|
|||
const headerClasses = {
|
||||
mx_GroupView_header: true,
|
||||
mx_GroupView_header_view: !this.state.editing,
|
||||
mx_GroupView_header_isUserMember: this.state.isUserMember,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -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(<GroupTile groupId={g} />);
|
||||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<div>
|
||||
<h3>{ _t('Your Communities') }</h3>
|
||||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</div> :
|
||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</GeminiScrollbar> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
"You're not currently a member of any communities.",
|
||||
|
@ -176,6 +176,7 @@ export default withMatrixClient(React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{ contentHeader }
|
||||
{ content }
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -43,8 +43,6 @@ const Rooms = require('../../Rooms');
|
|||
|
||||
import KeyCode from '../../KeyCode';
|
||||
|
||||
import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -539,12 +537,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) {
|
||||
|
@ -566,7 +558,6 @@ module.exports = React.createClass({
|
|||
this._warnAboutEncryption(room);
|
||||
this._calculatePeekRules(room);
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
UserProvider.getInstance().setUserListFromRoom(room);
|
||||
},
|
||||
|
||||
_warnAboutEncryption: function(room) {
|
||||
|
@ -690,9 +681,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.
|
||||
|
|
|
@ -166,7 +166,7 @@ module.exports = React.createClass({
|
|||
} else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
{ _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 }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={_t('I have verified my email address')} />
|
||||
|
|
|
@ -24,6 +24,7 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupName: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
|
@ -53,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 (
|
||||
<BaseAvatar
|
||||
name={this.props.groupId[1]}
|
||||
name={groupName || this.props.groupId[1]}
|
||||
idName={this.props.groupId}
|
||||
url={this.getGroupAvatarUrl()}
|
||||
{...otherProps}
|
||||
|
|
|
@ -36,6 +36,7 @@ export default React.createClass({
|
|||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
|
@ -75,7 +76,6 @@ export default React.createClass({
|
|||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': this.props.danger,
|
||||
|
@ -113,7 +113,7 @@ export default React.createClass({
|
|||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={title}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
|
|
|
@ -55,8 +55,8 @@ export default React.createClass({
|
|||
|
||||
_checkGroupId: function(e) {
|
||||
let error = null;
|
||||
if (!/^[a-zA-Z0-9]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain alphanumeric characters");
|
||||
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
|
@ -137,16 +137,19 @@ export default React.createClass({
|
|||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<span>+</span>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
<div className="mx_CreateGroupDialog_input_group">
|
||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size="32"
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span>:{ MatrixClientPeg.get().getDomain() }</span>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
||||
:{ MatrixClientPeg.get().getDomain() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 = <RoomAvatar room={room} width={16} height={16} />;
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -44,21 +44,21 @@ export default React.createClass({
|
|||
|
||||
const label = <EmojiText
|
||||
element="div"
|
||||
title={groupName}
|
||||
className="mx_GroupInviteTile_name"
|
||||
title={this.props.group.groupId}
|
||||
className="mx_RoomTile_name"
|
||||
dir="auto"
|
||||
>
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
|
||||
const badge = <div className="mx_GroupInviteTile_badge">!</div>;
|
||||
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_GroupInviteTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupInviteTile_avatarContainer">
|
||||
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_GroupInviteTile_nameContainer">
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
|
|
|
@ -17,50 +17,66 @@ 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 { GroupMemberType } from '../../../groups';
|
||||
import { groupMemberFromApiObject } from '../../../groups';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
|
||||
module.exports = withMatrixClient(React.createClass({
|
||||
module.exports = React.createClass({
|
||||
displayName: 'GroupMemberInfo',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
groupId: PropTypes.string,
|
||||
groupMember: GroupMemberType,
|
||||
isInvited: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
fetching: false,
|
||||
removingUser: false,
|
||||
groupMembers: null,
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._fetchMembers();
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
_fetchMembers: function() {
|
||||
this.setState({fetching: true});
|
||||
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
|
||||
this.setState({
|
||||
groupMembers: result.chunk.map((apiMember) => {
|
||||
return groupMemberFromApiObject(apiMember);
|
||||
}),
|
||||
fetching: false,
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.setState({fetching: false});
|
||||
console.error("Failed to get group groupMember list: ", e);
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore();
|
||||
this._initGroupStore(newProps.groupId);
|
||||
}
|
||||
},
|
||||
|
||||
_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);
|
||||
}
|
||||
},
|
||||
|
||||
onGroupStoreUpdated: function() {
|
||||
this.setState({
|
||||
isUserInvited: this._groupStore.getGroupInvitedMembers().some(
|
||||
(m) => m.userId === this.props.groupMember.userId,
|
||||
),
|
||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -68,13 +84,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
groupMember: this.props.groupMember,
|
||||
action: _t('Remove from community'),
|
||||
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;
|
||||
|
||||
this.setState({removingUser: true});
|
||||
this.props.matrixClient.removeUserFromGroup(
|
||||
this.context.matrixClient.removeUserFromGroup(
|
||||
this.props.groupId, this.props.groupMember.userId,
|
||||
).then(() => {
|
||||
// return to the user list
|
||||
|
@ -86,7 +104,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('Failed to remove user from community'),
|
||||
description: this.state.isUserInvited ?
|
||||
_t('Failed to withdraw invitation') :
|
||||
_t('Failed to remove user from community'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({removingUser: false});
|
||||
|
@ -111,24 +131,17 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.fetching || this.state.removingUser) {
|
||||
if (this.state.removingUser) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
if (!this.state.groupMembers) return null;
|
||||
|
||||
const targetIsInGroup = this.state.groupMembers.some((m) => {
|
||||
return m.userId === this.props.groupMember.userId;
|
||||
});
|
||||
|
||||
let kickButton;
|
||||
let adminButton;
|
||||
|
||||
if (targetIsInGroup) {
|
||||
kickButton = (
|
||||
let adminTools;
|
||||
if (this.state.isUserPrivilegedInGroup) {
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this._onKick}>
|
||||
{ _t('Remove from community') }
|
||||
{ this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
|
@ -137,22 +150,19 @@ module.exports = withMatrixClient(React.createClass({
|
|||
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</AccessibleButton>;*/
|
||||
|
||||
if (kickButton) {
|
||||
adminTools =
|
||||
<div className="mx_MemberInfo_adminTools">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{ kickButton }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
if (kickButton || adminButton) {
|
||||
adminTools =
|
||||
<div className="mx_MemberInfo_adminTools">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{ kickButton }
|
||||
{ adminButton }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = this.props.matrixClient.mxcUrlToHttp(
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.props.groupMember.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
@ -192,4 +202,4 @@ module.exports = withMatrixClient(React.createClass({
|
|||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
242
src/components/views/groups/GroupRoomInfo.js
Normal file
242
src/components/views/groups/GroupRoomInfo.js
Normal file
|
@ -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 <div className="mx_MemberInfo">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
if (this.state.isUserPrivilegedInGroup) {
|
||||
adminTools =
|
||||
<div className="mx_MemberInfo_adminTools">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
<AccessibleButton className="mx_MemberInfo_field" onClick={this._onRemove}>
|
||||
{ _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<h3>
|
||||
{ _t('Visibility in Room List') }
|
||||
{ this.state.groupRoomPublicityLoading ?
|
||||
<InlineSpinner /> : <div />
|
||||
}
|
||||
</h3>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value="public"
|
||||
checked={this.state.groupRoom.isPublic}
|
||||
onClick={this._changeGroupRoomPublicity}
|
||||
/>
|
||||
<div className="mx_MemberInfo_label_text">
|
||||
{ _t('Visible to everyone') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value="private"
|
||||
checked={!this.state.groupRoom.isPublic}
|
||||
onClick={this._changeGroupRoomPublicity}
|
||||
/>
|
||||
<div className="mx_MemberInfo_label_text">
|
||||
{ _t('Only visible to community members') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.state.groupRoom.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
||||
const groupRoomName = this.state.groupRoom.displayname;
|
||||
const avatar = <BaseAvatar name={groupRoomName} width={36} height={36} url={avatarUrl} />;
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbar autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
{ avatar }
|
||||
</div>
|
||||
|
||||
<EmojiText element="h2">{ groupRoomName }</EmojiText>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.state.groupRoom.canonical_alias }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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 = (
|
||||
<BaseAvatar name={this.state.name}
|
||||
<BaseAvatar name={this.props.groupRoom.displayname}
|
||||
width={36} height={36}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
|
@ -118,14 +58,8 @@ const GroupRoomTile = React.createClass({
|
|||
{ av }
|
||||
</div>
|
||||
<div className="mx_GroupRoomTile_name">
|
||||
{ this.state.name }
|
||||
{ this.props.groupRoom.displayname }
|
||||
</div>
|
||||
<AccessibleButton className="mx_GroupRoomTile_delete"
|
||||
onClick={this.onDeleteClick}
|
||||
tooltip={_t("Remove this room from the community")}
|
||||
>
|
||||
<img src="img/cancel.svg" width="15" height="15" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
<p>{ _t("An email has been sent to") } <i>{ this.props.inputs.emailAddress }</i></p>
|
||||
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p>
|
||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -370,7 +370,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("A text message has been sent to") } +<i>{ this._msisdn }</i></p>
|
||||
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p>
|
||||
<p>{ _t("Please enter the code it contains:") }</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
|
|
|
@ -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 <span />; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
// Name + flair
|
||||
const nameElem = [
|
||||
<EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>,
|
||||
props.enableFlair ?
|
||||
<Flair key='flair'
|
||||
userId={mxEvent.getSender()}
|
||||
roomId={mxEvent.getRoomId()}
|
||||
showRelated={true} />
|
||||
: 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 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
|
||||
nameElem,
|
||||
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : null,
|
||||
]);
|
||||
} else {
|
||||
content = nameElem;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
|
||||
<EmojiText className="mx_SenderProfile_name">{ name || '' }</EmojiText>
|
||||
{ props.enableFlair ?
|
||||
<Flair
|
||||
userId={mxEvent.getSender()}
|
||||
roomId={mxEvent.getRoomId()}
|
||||
showRelated={true} />
|
||||
: null
|
||||
}
|
||||
{ props.aux ? <EmojiText className="mx_SenderProfile_aux"> { props.aux }</EmojiText> : null }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ import ContextualMenu from '../../structures/ContextualMenu';
|
|||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -169,8 +170,10 @@ module.exports = React.createClass({
|
|||
|
||||
pillifyLinks: function(nodes) {
|
||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||
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 = <Pill
|
||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={true}
|
||||
/>;
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
/*
|
||||
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';
|
||||
import sdk from '../../../index';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import Promise from 'bluebird';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
|
||||
|
@ -17,6 +37,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 +62,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 +75,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 +113,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 +297,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),
|
||||
};
|
||||
|
|
|
@ -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 = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} enableFlair={!aux} aux={aux} />;
|
||||
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 = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} enableFlair={!text} text={text} />;
|
||||
} else {
|
||||
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 {
|
|||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
room={this.props.room}
|
||||
onConfirm={this.setDisplayedCompletion}
|
||||
onSelectionChange={this.setDisplayedCompletion}
|
||||
query={this.getAutocompleteQuery(content)}
|
||||
selection={selection} />
|
||||
selection={selection}
|
||||
/>
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||
|
|
|
@ -29,24 +29,27 @@ function getDisplayAliasForRoom(room) {
|
|||
}
|
||||
|
||||
const RoomDetailRow = React.createClass({
|
||||
propTypes: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
topic: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
avatarUrl: PropTypes.string,
|
||||
numJoinedMembers: PropTypes.number,
|
||||
canonicalAlias: PropTypes.string,
|
||||
aliases: PropTypes.arrayOf(PropTypes.string),
|
||||
propTypes: {
|
||||
room: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
topic: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
avatarUrl: PropTypes.string,
|
||||
numJoinedMembers: PropTypes.number,
|
||||
canonicalAlias: PropTypes.string,
|
||||
aliases: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
worldReadable: PropTypes.bool,
|
||||
guestCanJoin: PropTypes.bool,
|
||||
}),
|
||||
worldReadable: PropTypes.bool,
|
||||
guestCanJoin: PropTypes.bool,
|
||||
}),
|
||||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.room.roomId,
|
||||
room_alias: this.props.room.canonicalAlias || (this.props.room.aliases || [])[0],
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -34,27 +34,18 @@ const Receipt = require('../../../utils/Receipt');
|
|||
const HIDE_CONFERENCE_CHANS = true;
|
||||
|
||||
function phraseForSection(section) {
|
||||
// These would probably be better as individual strings,
|
||||
// but for some reason we have translations for these strings
|
||||
// as-is, so keeping it like this for now.
|
||||
let verb;
|
||||
switch (section) {
|
||||
case 'm.favourite':
|
||||
verb = _t('to favourite');
|
||||
break;
|
||||
return _t('Drop here to favourite');
|
||||
case 'im.vector.fake.direct':
|
||||
verb = _t('to tag direct chat');
|
||||
break;
|
||||
return _t('Drop here to tag direct chat');
|
||||
case 'im.vector.fake.recent':
|
||||
verb = _t('to restore');
|
||||
break;
|
||||
return _t('Drop here to restore');
|
||||
case 'm.lowpriority':
|
||||
verb = _t('to demote');
|
||||
break;
|
||||
return _t('Drop here to demote');
|
||||
default:
|
||||
return _t('Drop here to tag %(section)s', {section: section});
|
||||
}
|
||||
return _t('Drop here %(toAction)s', {toAction: verb});
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -564,13 +555,23 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
|
||||
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
|
||||
|
||||
const self = this;
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles()}
|
||||
label={_t('Community Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
isInvite={true}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
label={_t('Invites')}
|
||||
editable={false}
|
||||
|
@ -582,7 +583,6 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
extraTiles={inviteSectionExtraTiles}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
|
|
|
@ -83,10 +83,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_roomNameElement: function(fallback) {
|
||||
fallback = fallback || _t('a room');
|
||||
const name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
|
||||
return name ? name : fallback;
|
||||
_roomNameElement: function() {
|
||||
return this.props.room ? this.props.room.name : (this.props.room_alias || "");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -150,7 +148,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} 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 = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ _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.') }
|
||||
<br />
|
||||
{ _tJsx("<a>Click here</a> to join the discussion!",
|
||||
/<a>(.*?)<\/a>/,
|
||||
|
|
|
@ -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;
|
||||
|
@ -869,21 +870,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)') }
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="historyVis" value="invited"
|
||||
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
|
||||
checked={historyVisibility === "invited"}
|
||||
onChange={this._onHistoryRadioToggle} />
|
||||
{ _t('Members only') } ({ _t('since they were invited') })
|
||||
{ _t('Members only (since they were invited)') }
|
||||
</label>
|
||||
<label >
|
||||
<input type="radio" name="historyVis" value="joined"
|
||||
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
|
||||
checked={historyVisibility === "joined"}
|
||||
onChange={this._onHistoryRadioToggle} />
|
||||
{ _t('Members only') } ({ _t('since they joined') })
|
||||
{ _t('Members only (since they joined)') }
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -184,7 +184,8 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onClickChange: function() {
|
||||
onClickChange: function(ev) {
|
||||
ev.preventDefault();
|
||||
const oldPassword = this.state.cachedPassword || this.refs.old_input.value;
|
||||
const newPassword = this.refs.new_input.value;
|
||||
const confirmPassword = this.refs.confirm_input.value;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -152,13 +152,13 @@
|
|||
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
|
||||
"Communities": "Communities",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Mention": "Mention",
|
||||
"%(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?",
|
||||
|
@ -217,9 +217,9 @@
|
|||
" (unsupported)": " (unsupported)",
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
"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",
|
||||
|
@ -232,9 +232,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",
|
||||
|
@ -246,6 +250,7 @@
|
|||
"Unignore": "Unignore",
|
||||
"Ignore": "Ignore",
|
||||
"Jump to read receipt": "Jump to read receipt",
|
||||
"Mention": "Mention",
|
||||
"Invite": "Invite",
|
||||
"User Options": "User Options",
|
||||
"Direct chats": "Direct chats",
|
||||
|
@ -320,35 +325,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 <StartChatButton> to start a chat with someone": "Press <StartChatButton> to start a chat with someone",
|
||||
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> 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 <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> 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.",
|
||||
"<a>Click here</a> to join the discussion!": "<a>Click here</a> 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",
|
||||
|
@ -393,10 +399,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",
|
||||
|
@ -460,10 +465,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",
|
||||
|
@ -485,15 +490,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:",
|
||||
|
@ -513,56 +525,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",
|
||||
|
@ -570,7 +583,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",
|
||||
|
@ -584,8 +596,7 @@
|
|||
"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 alphanumeric characters": "Community IDs may only contain alphanumeric characters",
|
||||
"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",
|
||||
"Community Name": "Community Name",
|
||||
|
@ -828,7 +839,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",
|
||||
|
|
|
@ -251,6 +251,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(
|
||||
|
@ -260,7 +280,7 @@ function getLanguage(langPath) {
|
|||
reject({err: err, response: response});
|
||||
return;
|
||||
}
|
||||
resolve(JSON.parse(body));
|
||||
resolve(weblateToCounterpart(JSON.parse(body)));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -48,6 +48,9 @@ class FlairStore extends EventEmitter {
|
|||
// reject: () => {}
|
||||
// }
|
||||
};
|
||||
this._usersInFlight = {
|
||||
// This has the same schema as _usersPending
|
||||
};
|
||||
|
||||
this._debounceTimeoutID = null;
|
||||
}
|
||||
|
@ -66,7 +69,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 +94,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 +116,29 @@ 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) => {
|
||||
// The promise should always exist for userId, but do a null-check anyway
|
||||
if (!this._usersInFlight[userId]) return;
|
||||
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) => {
|
||||
// The promise should always exist for userId, but do a null-check anyway
|
||||
if (!this._usersInFlight[userId]) return;
|
||||
this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,23 @@ import FlairStore from './FlairStore';
|
|||
* other useful group APIs that may have an effect on the group summary.
|
||||
*/
|
||||
export default class GroupStore extends EventEmitter {
|
||||
|
||||
static STATE_KEY = {
|
||||
GroupMembers: 'GroupMembers',
|
||||
GroupInvitedMembers: 'GroupInvitedMembers',
|
||||
Summary: 'Summary',
|
||||
GroupRooms: 'GroupRooms',
|
||||
};
|
||||
|
||||
constructor(matrixClient, groupId) {
|
||||
super();
|
||||
this.groupId = groupId;
|
||||
this._matrixClient = matrixClient;
|
||||
this._summary = {};
|
||||
this._rooms = [];
|
||||
this._members = [];
|
||||
this._invitedMembers = [];
|
||||
this._ready = {};
|
||||
|
||||
this.on('error', (err) => {
|
||||
console.error(`GroupStore for ${this.groupId} encountered error`, err);
|
||||
|
@ -40,6 +51,7 @@ export default class GroupStore extends EventEmitter {
|
|||
this._members = result.chunk.map((apiMember) => {
|
||||
return groupMemberFromApiObject(apiMember);
|
||||
});
|
||||
this._ready[GroupStore.STATE_KEY.GroupMembers] = true;
|
||||
this._notifyListeners();
|
||||
}).catch((err) => {
|
||||
console.error("Failed to get group member list: " + err);
|
||||
|
@ -50,6 +62,7 @@ export default class GroupStore extends EventEmitter {
|
|||
this._invitedMembers = result.chunk.map((apiMember) => {
|
||||
return groupMemberFromApiObject(apiMember);
|
||||
});
|
||||
this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true;
|
||||
this._notifyListeners();
|
||||
}).catch((err) => {
|
||||
// Invited users not visible to non-members
|
||||
|
@ -64,6 +77,7 @@ export default class GroupStore extends EventEmitter {
|
|||
_fetchSummary() {
|
||||
this._matrixClient.getGroupSummary(this.groupId).then((resp) => {
|
||||
this._summary = resp;
|
||||
this._ready[GroupStore.STATE_KEY.Summary] = true;
|
||||
this._notifyListeners();
|
||||
}).catch((err) => {
|
||||
this.emit('error', err);
|
||||
|
@ -75,6 +89,7 @@ export default class GroupStore extends EventEmitter {
|
|||
this._rooms = resp.chunk.map((apiRoom) => {
|
||||
return groupRoomFromApiObject(apiRoom);
|
||||
});
|
||||
this._ready[GroupStore.STATE_KEY.GroupRooms] = true;
|
||||
this._notifyListeners();
|
||||
}).catch((err) => {
|
||||
this.emit('error', err);
|
||||
|
@ -87,6 +102,8 @@ export default class GroupStore extends EventEmitter {
|
|||
|
||||
registerListener(fn) {
|
||||
this.on('update', fn);
|
||||
// Call to set initial state (before fetching starts)
|
||||
this.emit('update');
|
||||
this._fetchSummary();
|
||||
this._fetchRooms();
|
||||
this._fetchMembers();
|
||||
|
@ -96,6 +113,10 @@ export default class GroupStore extends EventEmitter {
|
|||
this.removeListener('update', fn);
|
||||
}
|
||||
|
||||
isStateReady(id) {
|
||||
return this._ready[id];
|
||||
}
|
||||
|
||||
getSummary() {
|
||||
return this._summary;
|
||||
}
|
||||
|
@ -120,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));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue