Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/widgetrendering
This commit is contained in:
commit
70c4100350
27 changed files with 328 additions and 143 deletions
|
@ -208,7 +208,7 @@ const sanitizeHtmlParams = {
|
||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||||
// we don't want to allow images with `https?` `src`s.
|
// we don't want to allow images with `https?` `src`s.
|
||||||
if (!attribs.src.startsWith('mxc://')) {
|
if (!attribs.src || !attribs.src.startsWith('mxc://')) {
|
||||||
return { tagName, attribs: {}};
|
return { tagName, attribs: {}};
|
||||||
}
|
}
|
||||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
* 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 2016 Aviral Dasgupta
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
import RoomProvider from './RoomProvider';
|
import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
|
import NotifProvider from './NotifProvider';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
export type SelectionRange = {
|
export type SelectionRange = {
|
||||||
|
@ -43,43 +45,59 @@ const PROVIDERS = [
|
||||||
UserProvider,
|
UserProvider,
|
||||||
RoomProvider,
|
RoomProvider,
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
|
NotifProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
].map((completer) => completer.getInstance());
|
];
|
||||||
|
|
||||||
// Providers will get rejected if they take longer than this.
|
// Providers will get rejected if they take longer than this.
|
||||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
|
|
||||||
export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
export default class Autocompleter {
|
||||||
/* Note: That this waits for all providers to return is *intentional*
|
constructor(room) {
|
||||||
otherwise, we run into a condition where new completions are displayed
|
this.room = room;
|
||||||
while the user is interacting with the list, which makes it difficult
|
this.providers = PROVIDERS.map((p) => {
|
||||||
to predict whether an action will actually do what is intended
|
return new p(room);
|
||||||
*/
|
});
|
||||||
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();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return completionsList.filter(
|
destroy() {
|
||||||
(inspection) => inspection.isFulfilled(),
|
this.providers.forEach((p) => {
|
||||||
).map((completionsState, i) => {
|
p.destroy();
|
||||||
return {
|
});
|
||||||
completions: completionsState.value(),
|
}
|
||||||
provider: PROVIDERS[i],
|
|
||||||
|
|
||||||
/* the currently matched "command" the completer tried to complete
|
async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||||
* we pass this through so that Autocomplete can figure out when to
|
/* Note: This intentionally waits for all providers to return,
|
||||||
* re-show itself once hidden.
|
otherwise, we run into a condition where new completions are displayed
|
||||||
*/
|
while the user is interacting with the list, which makes it difficult
|
||||||
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
|
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 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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;
|
const COMMAND_RE = /(^\/\w*)/g;
|
||||||
|
|
||||||
let instance = null;
|
|
||||||
|
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(COMMAND_RE);
|
super(COMMAND_RE);
|
||||||
|
@ -142,12 +141,6 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
return '*️⃣ ' + _t('Commands');
|
return '*️⃣ ' + _t('Commands');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): CommandProvider {
|
|
||||||
if (instance === null) instance = new CommandProvider();
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_block">
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
{ completions }
|
{ completions }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||||
const REFERRER = 'vector';
|
const REFERRER = 'vector';
|
||||||
|
|
||||||
let instance = null;
|
|
||||||
|
|
||||||
export default class DuckDuckGoProvider extends AutocompleteProvider {
|
export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(DDG_REGEX);
|
super(DDG_REGEX);
|
||||||
|
@ -96,13 +95,6 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
return '🔍 ' + _t('Results from DuckDuckGo');
|
return '🔍 ' + _t('Results from DuckDuckGo');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): DuckDuckGoProvider {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new DuckDuckGoProvider();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_block">
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
{ completions }
|
{ completions }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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) {
|
function score(query, space) {
|
||||||
const index = space.indexOf(query);
|
const index = space.indexOf(query);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
@ -151,11 +150,6 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
return '😃 ' + _t('Emoji');
|
return '😃 ' + _t('Emoji');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
|
||||||
if (instance == null) {instance = new EmojiProvider();}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
{ completions }
|
{ completions }
|
||||||
|
|
62
src/autocomplete/NotifProvider.js
Normal file
62
src/autocomplete/NotifProvider.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
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 AutocompleteProvider from './AutocompleteProvider';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
import {PillCompletion} from './Components';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
|
const AT_ROOM_REGEX = /@\S*/g;
|
||||||
|
|
||||||
|
export default class NotifProvider extends AutocompleteProvider {
|
||||||
|
constructor(room) {
|
||||||
|
super(AT_ROOM_REGEX);
|
||||||
|
this.room = room;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return [];
|
||||||
|
|
||||||
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
|
if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) {
|
||||||
|
return [{
|
||||||
|
completion: '@room',
|
||||||
|
suffix: ' ',
|
||||||
|
component: (
|
||||||
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
|
||||||
|
),
|
||||||
|
range,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return '❗️ ' + _t('Room Notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
|
{ completions }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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;
|
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||||
|
|
||||||
let instance = null;
|
|
||||||
|
|
||||||
function score(query, space) {
|
function score(query, space) {
|
||||||
const index = space.indexOf(query);
|
const index = space.indexOf(query);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
@ -96,14 +95,6 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
return '💬 ' + _t('Rooms');
|
return '💬 ' + _t('Rooms');
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new RoomProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
{ completions }
|
{ completions }
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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;
|
const USER_REGEX = /@\S*/g;
|
||||||
|
|
||||||
let instance = null;
|
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
users: Array<RoomMember> = null;
|
users: Array<RoomMember> = null;
|
||||||
room: Room = null;
|
room: Room = null;
|
||||||
|
|
||||||
constructor() {
|
constructor(room) {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
keys: ['name'],
|
keys: ['name'],
|
||||||
});
|
});
|
||||||
|
this.room = room;
|
||||||
this.matcher = new FuzzyMatcher([], {
|
this.matcher = new FuzzyMatcher([], {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name', 'userId'],
|
||||||
shouldMatchPrefix: true,
|
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) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
|
@ -86,11 +122,6 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return '👥 ' + _t('Users');
|
return '👥 ' + _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserListFromRoom(room: Room) {
|
|
||||||
this.room = room;
|
|
||||||
this.users = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_makeUsers() {
|
_makeUsers() {
|
||||||
const events = this.room.getLiveTimeline().getEvents();
|
const events = this.room.getLiveTimeline().getEvents();
|
||||||
const lastSpoken = {};
|
const lastSpoken = {};
|
||||||
|
@ -123,13 +154,6 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.matcher.setObjects(this.users);
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): UserProvider {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new UserProvider();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
{ completions }
|
{ completions }
|
||||||
|
|
|
@ -15,13 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import { _t, _tJsx } from '../../languageHandler';
|
import { _t, _tJsx } from '../../languageHandler';
|
||||||
import withMatrixClient from '../../wrappers/withMatrixClient';
|
import withMatrixClient from '../../wrappers/withMatrixClient';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
|
|
||||||
import FlairStore from '../../stores/FlairStore';
|
import FlairStore from '../../stores/FlairStore';
|
||||||
|
@ -115,18 +116,17 @@ export default withMatrixClient(React.createClass({
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
let contentHeader;
|
||||||
if (this.state.groups) {
|
if (this.state.groups) {
|
||||||
const groupNodes = [];
|
const groupNodes = [];
|
||||||
this.state.groups.forEach((g) => {
|
this.state.groups.forEach((g) => {
|
||||||
groupNodes.push(<GroupTile groupId={g} />);
|
groupNodes.push(<GroupTile groupId={g} />);
|
||||||
});
|
});
|
||||||
|
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||||
content = groupNodes.length > 0 ?
|
content = groupNodes.length > 0 ?
|
||||||
<div>
|
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
||||||
<h3>{ _t('Your Communities') }</h3>
|
{ groupNodes }
|
||||||
<div className="mx_MyGroups_joinedGroups">
|
</GeminiScrollbar> :
|
||||||
{ groupNodes }
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
<div className="mx_MyGroups_placeholder">
|
<div className="mx_MyGroups_placeholder">
|
||||||
{ _t(
|
{ _t(
|
||||||
"You're not currently a member of any communities.",
|
"You're not currently a member of any communities.",
|
||||||
|
@ -176,6 +176,7 @@ export default withMatrixClient(React.createClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
|
{ contentHeader }
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -44,8 +44,6 @@ const Rooms = require('../../Rooms');
|
||||||
|
|
||||||
import KeyCode from '../../KeyCode';
|
import KeyCode from '../../KeyCode';
|
||||||
|
|
||||||
import UserProvider from '../../autocomplete/UserProvider';
|
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
|
|
||||||
|
@ -305,6 +303,15 @@ module.exports = React.createClass({
|
||||||
_shouldShowApps: function(room) {
|
_shouldShowApps: function(room) {
|
||||||
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
||||||
|
|
||||||
|
// Check if user has previously chosen to hide the app drawer for this
|
||||||
|
// room. If so, do not show apps
|
||||||
|
let hideWidgetDrawer = localStorage.getItem(
|
||||||
|
room.roomId + "_hide_widget_drawer");
|
||||||
|
|
||||||
|
if (hideWidgetDrawer === "true") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||||
// any valid widget = show apps
|
// any valid widget = show apps
|
||||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
for (let i = 0; i < appsStateEvents.length; i++) {
|
||||||
|
@ -541,12 +548,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) {
|
onRoomName: function(room) {
|
||||||
|
@ -568,7 +569,6 @@ module.exports = React.createClass({
|
||||||
this._warnAboutEncryption(room);
|
this._warnAboutEncryption(room);
|
||||||
this._calculatePeekRules(room);
|
this._calculatePeekRules(room);
|
||||||
this._updatePreviewUrlVisibility(room);
|
this._updatePreviewUrlVisibility(room);
|
||||||
UserProvider.getInstance().setUserListFromRoom(room);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_warnAboutEncryption: function(room) {
|
_warnAboutEncryption: function(room) {
|
||||||
|
@ -722,9 +722,6 @@ module.exports = React.createClass({
|
||||||
// refresh the conf call notification state
|
// refresh the conf call notification state
|
||||||
this._updateConfCallNotification();
|
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
|
// 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
|
// means we have finished joining a room we were previously peeking
|
||||||
// into.
|
// into.
|
||||||
|
|
|
@ -137,16 +137,19 @@ export default React.createClass({
|
||||||
<div className="mx_CreateGroupDialog_label">
|
<div className="mx_CreateGroupDialog_label">
|
||||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mx_CreateGroupDialog_input_group">
|
||||||
<span>+</span>
|
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
<input id="groupid"
|
||||||
|
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||||
size="32"
|
size="32"
|
||||||
placeholder={_t('example')}
|
placeholder={_t('example')}
|
||||||
onChange={this._onGroupIdChange}
|
onChange={this._onGroupIdChange}
|
||||||
onBlur={this._onGroupIdBlur}
|
onBlur={this._onGroupIdBlur}
|
||||||
value={this.state.groupId}
|
value={this.state.groupId}
|
||||||
/>
|
/>
|
||||||
<span>:{ MatrixClientPeg.get().getDomain() }</span>
|
<span className="mx_CreateGroupDialog_suffix">
|
||||||
|
:{ MatrixClientPeg.get().getDomain() }
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="error">
|
<div className="error">
|
||||||
|
|
|
@ -26,11 +26,9 @@ class MenuOption extends React.Component {
|
||||||
this._onClick = this._onClick.bind(this);
|
this._onClick = this._onClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultProps() {
|
static defaultProps = {
|
||||||
return {
|
disabled: false,
|
||||||
disabled: false,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMouseEnter() {
|
_onMouseEnter() {
|
||||||
this.props.onMouseEnter(this.props.dropdownKey);
|
this.props.onMouseEnter(this.props.dropdownKey);
|
||||||
|
|
|
@ -44,21 +44,21 @@ export default React.createClass({
|
||||||
|
|
||||||
const label = <EmojiText
|
const label = <EmojiText
|
||||||
element="div"
|
element="div"
|
||||||
title={groupName}
|
title={this.props.group.groupId}
|
||||||
className="mx_GroupInviteTile_name"
|
className="mx_RoomTile_name"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{ groupName }
|
{ groupName }
|
||||||
</EmojiText>;
|
</EmojiText>;
|
||||||
|
|
||||||
const badge = <div className="mx_GroupInviteTile_badge">!</div>;
|
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className="mx_GroupInviteTile" onClick={this.onClick}>
|
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
||||||
<div className="mx_GroupInviteTile_avatarContainer">
|
<div className="mx_RoomTile_avatar">
|
||||||
{ av }
|
{ av }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_GroupInviteTile_nameContainer">
|
<div className="mx_RoomTile_nameContainer">
|
||||||
{ label }
|
{ label }
|
||||||
{ badge }
|
{ badge }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -61,9 +61,9 @@ export default withMatrixClient(React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityTile presenceState="online"
|
<EntityTile name={name} avatarJsx={av} onClick={this.onClick}
|
||||||
avatarJsx={av} onClick={this.onClick}
|
suppressOnHover={true} presenceState="online"
|
||||||
name={name} powerLevel={0} suppressOnHover={true}
|
powerStatus={this.props.member.isAdmin ? EntityTile.POWER_STATUS_ADMIN : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -194,6 +194,9 @@ module.exports = React.createClass({
|
||||||
node.parentNode.replaceChild(pillContainer, node);
|
node.parentNode.replaceChild(pillContainer, node);
|
||||||
// Pills within pills aren't going to go well, so move on
|
// Pills within pills aren't going to go well, so move on
|
||||||
pillified = true;
|
pillified = true;
|
||||||
|
|
||||||
|
// update the current node with one that's now taken its place
|
||||||
|
node = pillContainer;
|
||||||
}
|
}
|
||||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
|
|
@ -81,16 +81,25 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(action) {
|
onAction: function(action) {
|
||||||
|
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'appsDrawer':
|
case 'appsDrawer':
|
||||||
// When opening the app draw when there aren't any apps, auto-launch the
|
// When opening the app drawer when there aren't any apps,
|
||||||
// integrations manager to skip the awkward click on "Add widget"
|
// auto-launch the integrations manager to skip the awkward
|
||||||
|
// click on "Add widget"
|
||||||
if (action.show) {
|
if (action.show) {
|
||||||
const apps = this._getApps();
|
const apps = this._getApps();
|
||||||
if (apps.length === 0) {
|
if (apps.length === 0) {
|
||||||
this._launchManageIntegrations();
|
this._launchManageIntegrations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(hideWidgetKey);
|
||||||
|
} else {
|
||||||
|
// Store hidden state of widget
|
||||||
|
// Don't show if previously hidden
|
||||||
|
localStorage.setItem(hideWidgetKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 Aviral Dasgupta
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import flatMap from 'lodash/flatMap';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
@ -7,8 +25,9 @@ import sdk from '../../../index';
|
||||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
const COMPOSER_SELECTED = 0;
|
const COMPOSER_SELECTED = 0;
|
||||||
|
|
||||||
|
@ -17,6 +36,7 @@ export default class Autocomplete extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.autocompleter = new Autocompleter(props.room);
|
||||||
this.completionPromise = null;
|
this.completionPromise = null;
|
||||||
this.hide = this.hide.bind(this);
|
this.hide = this.hide.bind(this);
|
||||||
this.onCompletionClicked = this.onCompletionClicked.bind(this);
|
this.onCompletionClicked = this.onCompletionClicked.bind(this);
|
||||||
|
@ -41,6 +61,11 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(newProps, state) {
|
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
|
// Query hasn't changed so don't try to complete it
|
||||||
if (newProps.query === this.props.query) {
|
if (newProps.query === this.props.query) {
|
||||||
return;
|
return;
|
||||||
|
@ -49,6 +74,10 @@ export default class Autocomplete extends React.Component {
|
||||||
this.complete(newProps.query, newProps.selection);
|
this.complete(newProps.query, newProps.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.autocompleter.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
complete(query, selection) {
|
complete(query, selection) {
|
||||||
this.queryRequested = query;
|
this.queryRequested = query;
|
||||||
if (this.debounceCompletionsRequest) {
|
if (this.debounceCompletionsRequest) {
|
||||||
|
@ -83,7 +112,7 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
processQuery(query, selection) {
|
processQuery(query, selection) {
|
||||||
return getCompletions(
|
return this.autocompleter.getCompletions(
|
||||||
query, selection, this.state.forceComplete,
|
query, selection, this.state.forceComplete,
|
||||||
).then((completions) => {
|
).then((completions) => {
|
||||||
// Only ever process the completions for the most recent query being processed
|
// Only ever process the completions for the most recent query being processed
|
||||||
|
@ -267,8 +296,11 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
Autocomplete.propTypes = {
|
Autocomplete.propTypes = {
|
||||||
// the query string for which to show autocomplete suggestions
|
// 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
|
// 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),
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,7 +47,7 @@ function presenceClassForMember(presenceState, lastActiveAgo) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
const EntityTile = React.createClass({
|
||||||
displayName: 'EntityTile',
|
displayName: 'EntityTile',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -140,16 +140,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
let power;
|
let power;
|
||||||
const powerLevel = this.props.powerLevel;
|
const powerStatus = this.props.powerStatus;
|
||||||
if (powerLevel >= 50 && powerLevel < 99) {
|
if (powerStatus) {
|
||||||
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")} />;
|
const src = {
|
||||||
}
|
[EntityTile.POWER_STATUS_MODERATOR]: "img/mod.svg",
|
||||||
if (powerLevel >= 99) {
|
[EntityTile.POWER_STATUS_ADMIN]: "img/admin.svg",
|
||||||
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")} />;
|
}[powerStatus];
|
||||||
|
const alt = {
|
||||||
|
[EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
|
||||||
|
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
|
||||||
|
}[powerStatus];
|
||||||
|
power = <img src={src} className="mx_EntityTile_power" width="16" height="17" alt={alt} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
|
||||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||||
|
@ -168,3 +171,9 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EntityTile.POWER_STATUS_MODERATOR = "moderator";
|
||||||
|
EntityTile.POWER_STATUS_ADMIN = "admin";
|
||||||
|
|
||||||
|
|
||||||
|
export default EntityTile;
|
||||||
|
|
|
@ -86,13 +86,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
this.member_last_modified_time = member.getLastModifiedTime();
|
this.member_last_modified_time = member.getLastModifiedTime();
|
||||||
|
|
||||||
|
// We deliberately leave power levels that are not 100 or 50 undefined
|
||||||
|
const powerStatus = {
|
||||||
|
100: EntityTile.POWER_STATUS_ADMIN,
|
||||||
|
50: EntityTile.POWER_STATUS_MODERATOR,
|
||||||
|
}[this.props.member.powerLevel];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityTile {...this.props} presenceState={presenceState}
|
<EntityTile {...this.props} presenceState={presenceState}
|
||||||
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
||||||
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
||||||
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
||||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||||
name={name} powerLevel={this.props.member.powerLevel} />
|
name={name} powerStatus={powerStatus} />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -57,6 +58,11 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
const ZWS_CODE = 8203;
|
const ZWS_CODE = 8203;
|
||||||
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
||||||
|
|
||||||
|
const ENTITY_TYPES = {
|
||||||
|
AT_ROOM_PILL: 'ATROOMPILL',
|
||||||
|
};
|
||||||
|
|
||||||
function stateToMarkdown(state) {
|
function stateToMarkdown(state) {
|
||||||
return __stateToMarkdown(state)
|
return __stateToMarkdown(state)
|
||||||
.replace(
|
.replace(
|
||||||
|
@ -187,13 +193,16 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.client = MatrixClientPeg.get();
|
this.client = MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
||||||
contentBlock.findEntityRanges(
|
contentBlock.findEntityRanges(
|
||||||
(character) => {
|
(character) => {
|
||||||
const entityKey = character.getEntity();
|
const entityKey = character.getEntity();
|
||||||
return (
|
return (
|
||||||
entityKey !== null &&
|
entityKey !== null &&
|
||||||
contentState.getEntity(entityKey).getType() === 'LINK'
|
(
|
||||||
|
contentState.getEntity(entityKey).getType() === 'LINK' ||
|
||||||
|
contentState.getEntity(entityKey).getType() === ENTITY_TYPES.AT_ROOM_PILL
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}, callback,
|
}, callback,
|
||||||
);
|
);
|
||||||
|
@ -209,11 +218,19 @@ export default class MessageComposerInput extends React.Component {
|
||||||
RichText.getScopedMDDecorators(this.props);
|
RichText.getScopedMDDecorators(this.props);
|
||||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||||
decorators.push({
|
decorators.push({
|
||||||
strategy: this.findLinkEntities.bind(this),
|
strategy: this.findPillEntities.bind(this),
|
||||||
component: (entityProps) => {
|
component: (entityProps) => {
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
const type = entityProps.contentState.getEntity(entityProps.entityKey).getType();
|
||||||
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
|
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
|
||||||
if (Pill.isPillUrl(url)) {
|
if (type === ENTITY_TYPES.AT_ROOM_PILL) {
|
||||||
|
return <Pill
|
||||||
|
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||||
|
room={this.props.room}
|
||||||
|
offsetKey={entityProps.offsetKey}
|
||||||
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
/>;
|
||||||
|
} else if (Pill.isPillUrl(url)) {
|
||||||
return <Pill
|
return <Pill
|
||||||
url={url}
|
url={url}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
@ -783,7 +800,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const pt = contentState.getBlocksAsArray().map((block) => {
|
const pt = contentState.getBlocksAsArray().map((block) => {
|
||||||
let blockText = block.getText();
|
let blockText = block.getText();
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
this.findLinkEntities(contentState, block, (start, end) => {
|
this.findPillEntities(contentState, block, (start, end) => {
|
||||||
const entity = contentState.getEntity(block.getEntityAt(start));
|
const entity = contentState.getEntity(block.getEntityAt(start));
|
||||||
if (entity.getType() !== 'LINK') {
|
if (entity.getType() !== 'LINK') {
|
||||||
return;
|
return;
|
||||||
|
@ -988,6 +1005,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
isCompletion: true,
|
isCompletion: true,
|
||||||
});
|
});
|
||||||
entityKey = contentState.getLastCreatedEntityKey();
|
entityKey = contentState.getLastCreatedEntityKey();
|
||||||
|
} else if (completion === '@room') {
|
||||||
|
contentState = contentState.createEntity(ENTITY_TYPES.AT_ROOM_PILL, 'IMMUTABLE', {
|
||||||
|
isCompletion: true,
|
||||||
|
});
|
||||||
|
entityKey = contentState.getLastCreatedEntityKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection;
|
let selection;
|
||||||
|
@ -1130,10 +1152,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
|
room={this.props.room}
|
||||||
onConfirm={this.setDisplayedCompletion}
|
onConfirm={this.setDisplayedCompletion}
|
||||||
onSelectionChange={this.setDisplayedCompletion}
|
onSelectionChange={this.setDisplayedCompletion}
|
||||||
query={this.getAutocompleteQuery(content)}
|
query={this.getAutocompleteQuery(content)}
|
||||||
selection={selection} />
|
selection={selection}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||||
|
|
|
@ -49,6 +49,7 @@ const RoomDetailRow = React.createClass({
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
|
room_alias: this.props.room.canonicalAlias || (this.props.room.aliases || [])[0],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -555,13 +555,23 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||||
|
|
||||||
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
|
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
return (
|
return (
|
||||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||||
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
||||||
<div className="mx_RoomList">
|
<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']}
|
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||||
label={_t('Invites')}
|
label={_t('Invites')}
|
||||||
editable={false}
|
editable={false}
|
||||||
|
@ -573,7 +583,6 @@ module.exports = React.createClass({
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms}
|
onShowMoreRooms={self.onShowMoreRooms}
|
||||||
extraTiles={inviteSectionExtraTiles}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['m.favourite']}
|
<RoomSubList list={self.state.lists['m.favourite']}
|
||||||
|
|
|
@ -36,6 +36,7 @@ export function groupMemberFromApiObject(apiObject) {
|
||||||
userId: apiObject.user_id,
|
userId: apiObject.user_id,
|
||||||
displayname: apiObject.displayname,
|
displayname: apiObject.displayname,
|
||||||
avatarUrl: apiObject.avatar_url,
|
avatarUrl: apiObject.avatar_url,
|
||||||
|
isAdmin: apiObject.is_admin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -326,6 +326,7 @@
|
||||||
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
||||||
"Press <StartChatButton> to start a chat with someone": "Press <StartChatButton> to start a chat with someone",
|
"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",
|
"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",
|
"Invites": "Invites",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
"People": "People",
|
"People": "People",
|
||||||
|
@ -891,6 +892,8 @@
|
||||||
"Commands": "Commands",
|
"Commands": "Commands",
|
||||||
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
||||||
"Emoji": "Emoji",
|
"Emoji": "Emoji",
|
||||||
|
"Notify the whole room": "Notify the whole room",
|
||||||
|
"Room Notification": "Room Notification",
|
||||||
"Users": "Users",
|
"Users": "Users",
|
||||||
"unknown device": "unknown device",
|
"unknown device": "unknown device",
|
||||||
"NOT verified": "NOT verified",
|
"NOT verified": "NOT verified",
|
||||||
|
|
|
@ -48,6 +48,9 @@ class FlairStore extends EventEmitter {
|
||||||
// reject: () => {}
|
// reject: () => {}
|
||||||
// }
|
// }
|
||||||
};
|
};
|
||||||
|
this._usersInFlight = {
|
||||||
|
// This has the same schema as _usersPending
|
||||||
|
};
|
||||||
|
|
||||||
this._debounceTimeoutID = null;
|
this._debounceTimeoutID = null;
|
||||||
}
|
}
|
||||||
|
@ -125,12 +128,16 @@ class FlairStore extends EventEmitter {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Propagate the same error to all usersInFlight
|
// Propagate the same error to all usersInFlight
|
||||||
Object.keys(this._usersInFlight).forEach((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].reject(err);
|
this._usersInFlight[userId].reject(err);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updatedUserGroups = resp.users;
|
const updatedUserGroups = resp.users;
|
||||||
Object.keys(this._usersInFlight).forEach((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] || []);
|
this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,9 @@ export default class GroupStore extends EventEmitter {
|
||||||
|
|
||||||
constructor(matrixClient, groupId) {
|
constructor(matrixClient, groupId) {
|
||||||
super();
|
super();
|
||||||
|
if (!groupId) {
|
||||||
|
throw new Error('GroupStore needs a valid groupId to be created');
|
||||||
|
}
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this._matrixClient = matrixClient;
|
this._matrixClient = matrixClient;
|
||||||
this._summary = {};
|
this._summary = {};
|
||||||
|
|
Loading…
Reference in a new issue