-
+ const loadingElement = (
+
+
+
+ );
+ if (!this.state.hasPermissionToLoad) {
+ // only possible for room widgets, can assert this.props.room here
+ const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
+ appTileBody = (
+
);
- if (!this.state.hasPermissionToLoad) {
- const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
+ } else if (this.state.initialising) {
+ appTileBody = (
+
+ { loadingElement }
+
+ );
+ } else {
+ if (this.isMixedContent()) {
appTileBody = (
- );
- } else if (this.state.initialising) {
- appTileBody = (
-
);
} else {
- if (this.isMixedContent()) {
- appTileBody = (
-
- );
- } else {
- appTileBody = (
-
- { this.state.loading && loadingElement }
-
-
- );
- // if the widget would be allowed to remain on screen, we must put it in
- // a PersistedElement from the get-go, otherwise the iframe will be
- // re-mounted later when we do.
- if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
- const PersistedElement = sdk.getComponent("elements.PersistedElement");
- // Also wrap the PersistedElement in a div to fix the height, otherwise
- // AppTile's border is in the wrong place
- appTileBody =
;
- }
+ appTileBody = (
+
+ { this.state.loading && loadingElement }
+
+
+ );
+ // if the widget would be allowed to remain on screen, we must put it in
+ // a PersistedElement from the get-go, otherwise the iframe will be
+ // re-mounted later when we do.
+ if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
+ const PersistedElement = sdk.getComponent("elements.PersistedElement");
+ // Also wrap the PersistedElement in a div to fix the height, otherwise
+ // AppTile's border is in the wrong place
+ appTileBody =
;
}
}
}
- const showMinimiseButton = this.props.showMinimise && this.props.show;
- const showMaximiseButton = this.props.showMinimise && !this.props.show;
-
let appTileClasses;
if (this.props.miniMode) {
appTileClasses = {mx_AppTile_mini: true};
@@ -553,73 +399,37 @@ export default class AppTile extends React.Component {
} else {
appTileClasses = {mx_AppTile: true};
}
- appTileClasses.mx_AppTile_minimised = !this.props.show;
appTileClasses = classNames(appTileClasses);
- const menuBarClasses = classNames({
- mx_AppTileMenuBar: true,
- mx_AppTileMenuBar_expanded: this.props.show,
- });
-
let contextMenu;
if (this.state.menuDisplayed) {
- const elementRect = this._contextMenuButton.current.getBoundingClientRect();
-
- const canUserModify = this._canUserModify();
- const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
- const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
- const showPictureSnapshotButton = this.props.show && this._sgWidget.widgetApi &&
- this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots);
-
- const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
contextMenu = (
-
-
-
+
);
}
return
{ this.props.showMenubar &&
-
+
- { /* Minimise widget */ }
- { showMinimiseButton && }
- { /* Maximise widget */ }
- { showMaximiseButton && }
- { /* Title */ }
{ this.props.showTitle && this._getTileTitle() }
- { /* Popout widget */ }
{ this.props.showPopout && }
- { /* Context menu */ }
{
- {this.props.children}
- ;
+ const content =
+
+ {this.props.children}
+
+ ;
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
}
@@ -173,3 +177,5 @@ export default class PersistedElement extends React.Component {
return
;
}
}
+
+export const getPersistKey = (appId: string) => 'widget_' + appId;
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index a3e413151a..64825dfc96 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -58,6 +58,11 @@ export default class PersistentApp extends React.Component {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (this.state.roomId !== persistentWidgetInRoomId) {
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
+
+ // Sanity check the room - the widget may have been destroyed between render cycles, and
+ // thus no room is associated anymore.
+ if (!persistentWidgetInRoom) return null;
+
// get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
@@ -74,13 +79,10 @@ export default class PersistentApp extends React.Component {
fullWidth={true}
room={persistentWidgetInRoom}
userId={MatrixClientPeg.get().credentials.userId}
- show={true}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
- showDelete={false}
- showMinimise={false}
miniMode={true}
showMenubar={false}
/>;
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index b1a526eb0d..6c9a01a840 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -26,12 +26,12 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
-import TagOrderStore from '../../../stores/TagOrderStore';
+import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
-// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
+// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
@@ -142,7 +142,7 @@ export default class TagTile extends React.Component {
mx_TagTile_selected_prototype: this.props.selected && isPrototype,
});
- const badge = TagOrderStore.getGroupBadge(this.props.tag);
+ const badge = GroupFilterOrderStore.getGroupBadge(this.props.tag);
let badgeElement;
if (badge && !this.state.hover && !this.props.menuDisplayed) {
const badgeClasses = classNames({
diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx
index 6b71a1dbf5..03b9eb08d0 100644
--- a/src/components/views/elements/Tooltip.tsx
+++ b/src/components/views/elements/Tooltip.tsx
@@ -36,6 +36,7 @@ interface IProps {
// the react element to put into the tooltip
label: React.ReactNode;
forceOnRight?: boolean;
+ yOffset?: number;
}
export default class Tooltip extends React.Component
{
@@ -46,6 +47,7 @@ export default class Tooltip extends React.Component {
public static readonly defaultProps = {
visible: true,
+ yOffset: 0,
};
// Create a wrapper for the tooltip outside the parent and attach it to the body element
@@ -82,9 +84,9 @@ export default class Tooltip extends React.Component {
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
}
- style.top = (parentBox.top - 2) + window.pageYOffset + offset;
+ style.top = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset + offset;
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
- style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8;
+ style.right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
} else {
style.left = parentBox.right + window.pageXOffset + 6;
}
diff --git a/src/components/views/elements/UserTagTile.tsx b/src/components/views/elements/UserTagTile.tsx
index 912f54edc7..e7c74bb10e 100644
--- a/src/components/views/elements/UserTagTile.tsx
+++ b/src/components/views/elements/UserTagTile.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import * as fbEmitter from "fbemitter";
-import TagOrderStore from "../../../stores/TagOrderStore";
+import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
@@ -36,12 +36,12 @@ export default class UserTagTile extends React.PureComponent {
super(props);
this.state = {
- selected: TagOrderStore.getSelectedTags().length === 0,
+ selected: GroupFilterOrderStore.getSelectedTags().length === 0,
};
}
public componentDidMount() {
- this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
+ this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
@@ -49,7 +49,7 @@ export default class UserTagTile extends React.PureComponent {
}
private onTagStoreUpdate = () => {
- const selected = TagOrderStore.getSelectedTags().length === 0;
+ const selected = GroupFilterOrderStore.getSelectedTags().length === 0;
this.setState({selected});
};
diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js
index 22d5ebba1e..6098b1217e 100644
--- a/src/components/views/messages/RoomCreate.js
+++ b/src/components/views/messages/RoomCreate.js
@@ -45,7 +45,7 @@ export default class RoomCreate extends React.Component {
render() {
const predecessor = this.props.mxEvent.getContent()['predecessor'];
if (predecessor === undefined) {
- return ; // We should never have been instaniated in this case
+ return ; // We should never have been instantiated in this case
}
const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']);
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx
index ff092ca060..7f682e2d89 100644
--- a/src/components/views/right_panel/HeaderButton.tsx
+++ b/src/components/views/right_panel/HeaderButton.tsx
@@ -31,7 +31,7 @@ interface IProps {
// The badge to display above the icon
badge?: React.ReactNode;
// The parameters to track the click event
- analytics: string[];
+ analytics: Parameters;
// Button name
name: string;
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 95b159deed..621e85e1d4 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, {useCallback, useState, useEffect, useContext} from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useIsEncrypted } from '../../../hooks/useIsEncrypted';
@@ -32,17 +31,18 @@ import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPa
import Modal from "../../../Modal";
import ShareDialog from '../dialogs/ShareDialog';
import {useEventEmitter} from "../../../hooks/useEventEmitter";
-import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import TextWithTooltip from "../elements/TextWithTooltip";
-import BaseAvatar from "../avatars/BaseAvatar";
+import WidgetAvatar from "../avatars/WidgetAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import WidgetStore, {IApp} from "../../../stores/WidgetStore";
+import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore";
import { E2EStatus } from "../../../utils/ShieldUtils";
import RoomContext from "../../../contexts/RoomContext";
import {UIFeature} from "../../../settings/UIFeature";
+import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
+import WidgetContextMenu from "../context_menus/WidgetContextMenu";
interface IProps {
room: Room;
@@ -68,22 +68,105 @@ const Button: React.FC = ({ children, className, onClick }) => {
};
export const useWidgets = (room: Room) => {
- const [apps, setApps] = useState(WidgetStore.instance.getApps(room));
+ const [apps, setApps] = useState(WidgetStore.instance.getApps(room.roomId));
const updateApps = useCallback(() => {
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
- setApps([...WidgetStore.instance.getApps(room)]);
+ setApps([...WidgetStore.instance.getApps(room.roomId)]);
}, [room]);
useEffect(updateApps, [room]);
- useEventEmitter(WidgetEchoStore, "update", updateApps);
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
return apps;
};
+interface IAppRowProps {
+ app: IApp;
+}
+
+const AppRow: React.FC = ({ app }) => {
+ const name = WidgetUtils.getWidgetName(app);
+ const dataTitle = WidgetUtils.getWidgetDataTitle(app);
+ const subtitle = dataTitle && " - " + dataTitle;
+
+ const onOpenWidgetClick = () => {
+ defaultDispatcher.dispatch({
+ action: Action.SetRightPanelPhase,
+ phase: RightPanelPhases.Widget,
+ refireParams: {
+ widgetId: app.id,
+ },
+ });
+ };
+
+ const isPinned = WidgetStore.instance.isPinned(app.id);
+ const togglePin = isPinned
+ ? () => { WidgetStore.instance.unpinWidget(app.id); }
+ : () => { WidgetStore.instance.pinWidget(app.id); };
+
+ const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
+ let contextMenu;
+ if (menuDisplayed) {
+ const rect = handle.current.getBoundingClientRect();
+ contextMenu = ;
+ }
+
+ const cannotPin = !isPinned && !WidgetStore.instance.canPin(app.id);
+
+ let pinTitle: string;
+ if (cannotPin) {
+ pinTitle = _t("You can only pin up to %(count)s widgets", { count: MAX_PINNED });
+ } else {
+ pinTitle = isPinned ? _t("Unpin") : _t("Pin");
+ }
+
+ const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
+ mx_RoomSummaryCard_Button_pinned: isPinned,
+ });
+
+ return
+
+
+ {name}
+ { subtitle }
+
+
+
+
+
+
+ { contextMenu }
+
;
+};
+
const AppsSection: React.FC = ({ room }) => {
- const cli = useContext(MatrixClientContext);
const apps = useWidgets(room);
const onManageIntegrations = () => {
@@ -100,65 +183,7 @@ const AppsSection: React.FC = ({ room }) => {
};
return
- { apps.map(app => {
- const name = WidgetUtils.getWidgetName(app);
- const dataTitle = WidgetUtils.getWidgetDataTitle(app);
- const subtitle = dataTitle && " - " + dataTitle;
-
- let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
- // heuristics for some better icons until Widgets support their own icons
- if (app.type.includes("meeting") || app.type.includes("calendar")) {
- iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")];
- } else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) {
- iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")];
- } else if (app.type.includes("clock")) {
- iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")];
- }
-
- if (app.avatar_url) { // MSC2765
- iconUrls.unshift(getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop"));
- }
-
- const isPinned = WidgetStore.instance.isPinned(app.id);
- const classes = classNames("mx_RoomSummaryCard_icon_app", {
- mx_RoomSummaryCard_icon_app_pinned: isPinned,
- });
-
- if (isPinned) {
- const onClick = () => {
- WidgetStore.instance.unpinWidget(app.id);
- };
-
- return
-
- {name}
- { subtitle }
-
- }
-
- const onOpenWidgetClick = () => {
- defaultDispatcher.dispatch({
- action: Action.SetRightPanelPhase,
- phase: RightPanelPhases.Widget,
- refireParams: {
- widgetId: app.id,
- },
- });
- };
-
- return (
-
- );
- }) }
+ { apps.map(app => ) }
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 807bd27796..dd291b1fa1 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -801,6 +801,11 @@ const RoomAdminToolsContainer: React.FC = ({
} = powerLevels;
const me = room.getMember(cli.getUserId());
+ if (!me) {
+ // we aren't in the room, so return no admin tooling
+ return ;
+ }
+
const isMe = me.userId === member.userId;
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 30900b9a4d..7dbb77df18 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -20,7 +20,6 @@ import {Room} from "matrix-js-sdk/src/models/room";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import BaseCard from "./BaseCard";
import WidgetUtils from "../../../utils/WidgetUtils";
-import AccessibleButton from "../elements/AccessibleButton";
import AppTile from "../elements/AppTile";
import {_t} from "../../../languageHandler";
import {useWidgets} from "./RoomSummaryCard";
@@ -30,16 +29,7 @@ import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPa
import {Action} from "../../../dispatcher/actions";
import WidgetStore from "../../../stores/WidgetStore";
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
-import IconizedContextMenu, {
- IconizedContextMenuOption,
- IconizedContextMenuOptionList,
-} from "../context_menus/IconizedContextMenu";
-import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
-import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import classNames from "classnames";
-import dis from "../../../dispatcher/dispatcher";
-import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
-import { MatrixCapabilities } from "matrix-widget-api";
+import WidgetContextMenu from "../context_menus/WidgetContextMenu";
interface IProps {
room: Room;
@@ -69,111 +59,22 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => {
// Don't render anything as we are about to transition
if (!app || isPinned) return null;
- const header =
- { WidgetUtils.getWidgetName(app) }
- ;
-
- const canModify = WidgetUtils.canUserModifyWidgets(room.roomId);
-
let contextMenu;
if (menuDisplayed) {
- let snapshotButton;
- const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
- if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
- const onSnapshotClick = () => {
- widgetMessaging.takeScreenshot().then(data => {
- dis.dispatch({
- action: 'picture_snapshot',
- file: data.screenshot,
- });
- }).catch(err => {
- console.error("Failed to take screenshot: ", err);
- });
- closeMenu();
- };
-
- snapshotButton = ;
- }
-
- let deleteButton;
- if (canModify) {
- const onDeleteClick = () => {
- defaultDispatcher.dispatch({
- action: Action.AppTileDelete,
- widgetId: app.id,
- });
- closeMenu();
- };
-
- deleteButton = ;
- }
-
- const onRevokeClick = () => {
- defaultDispatcher.dispatch({
- action: Action.AppTileRevoke,
- widgetId: app.id,
- });
- closeMenu();
- };
-
const rect = handle.current.getBoundingClientRect();
contextMenu = (
-
-
- { snapshotButton }
- { deleteButton }
-
-
-
+ app={app}
+ />
);
}
- const onPinClick = () => {
- WidgetStore.instance.pinWidget(app.id);
- };
-
- const onEditClick = () => {
- WidgetUtils.editWidget(room, app);
- };
-
- let editButton;
- if (canModify) {
- editButton =
- { _t("Edit") }
- ;
- }
-
- const pinButtonClasses = canModify ? "" : "mx_WidgetCard_widePinButton";
-
- let pinButton;
- if (WidgetStore.instance.canPin(app.id)) {
- pinButton =
- { _t("Pin to room") }
- ;
- } else {
- pinButton =
- { _t("Pin to room") }
- ;
- }
-
- const footer =
- { editButton }
- { pinButton }
+ const header =
+ { WidgetUtils.getWidgetName(app) }
= ({ room, widgetId, onClose }) => {
isExpanded={menuDisplayed}
label={_t("Options")}
/>
-
{ contextMenu }
;
return {
+ this.setState({ resizing });
+ if (!resizing) {
+ this._relaxResizer();
+ }
+ };
+
+ _createResizer() {
+ const classNames = {
+ handle: "mx_ResizeHandle",
+ vertical: "mx_ResizeHandle_vertical",
+ reverse: "mx_ResizeHandle_reverse",
+ };
+ const collapseConfig = {
+ onResizeStart: () => {
+ this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
+ },
+ onResizeStop: () => {
+ this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
+ // persist to localStorage
+ localStorage.setItem(this._getStorageKey(), JSON.stringify([
+ this.state.apps.map(app => app.id),
+ ...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
+ ]));
+ },
+ };
+ // pass a truthy container for now, we won't call attach until we update it
+ const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
+ resizer.setClassNames(classNames);
+ return resizer;
+ }
+
+ _collectResizer = (ref) => {
+ if (this._resizeContainer) {
+ this.resizer.detach();
+ }
+
+ if (ref) {
+ this.resizer.container = ref;
+ this.resizer.attach();
+ }
+ this._resizeContainer = ref;
+ this._loadResizerPreferences();
+ };
+
+ _getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
+
+ _getAppsHash = (apps) => apps.map(app => app.id).join("~");
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
+ this._loadResizerPreferences();
+ }
+ }
+
+ _relaxResizer = () => {
+ const distributors = this.resizer.getDistributors();
+
+ // relax all items if they had any overconstrained flexboxes
+ distributors.forEach(d => d.start());
+ distributors.forEach(d => d.finish());
+ };
+
+ _loadResizerPreferences = () => {
+ try {
+ const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
+ // Every app was included in the last split, reuse the last sizes
+ if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
+ sizes.forEach((size, i) => {
+ const distributor = this.resizer.forHandleAt(i);
+ if (distributor) {
+ distributor.size = size;
+ distributor.finish();
+ }
+ });
+ return;
+ }
+ } catch (e) {
+ // this is expected
+ }
+
+ if (this.state.apps) {
+ const distributors = this.resizer.getDistributors();
+ distributors.forEach(d => d.item.clearSize());
+ distributors.forEach(d => d.start());
+ distributors.forEach(d => d.finish());
+ }
+ };
+
onAction = (action) => {
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) {
@@ -93,7 +190,7 @@ export default class AppsDrawer extends React.Component {
}
};
- _getApps = () => WidgetStore.instance.getApps(this.props.room, true);
+ _getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
_updateApps = () => {
this.setState({
@@ -101,15 +198,6 @@ export default class AppsDrawer extends React.Component {
});
};
- _canUserModify() {
- try {
- return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
- } catch (err) {
- console.error(err);
- return false;
- }
- }
-
_launchManageIntegrations() {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
@@ -118,12 +206,9 @@ export default class AppsDrawer extends React.Component {
}
}
- onClickAddWidget = (e) => {
- e.preventDefault();
- this._launchManageIntegrations();
- };
-
render() {
+ if (!this.props.showApps) return ;
+
const apps = this.state.apps.map((app, index, arr) => {
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
@@ -133,7 +218,6 @@ export default class AppsDrawer extends React.Component {
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
- show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
@@ -145,21 +229,6 @@ export default class AppsDrawer extends React.Component {
return ;
}
- let addWidget;
- if (this.props.showApps &&
- this._canUserModify()
- ) {
- addWidget =
- [+] { _t('Add a widget') }
- ;
- }
-
let spinner;
if (
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
@@ -172,10 +241,11 @@ export default class AppsDrawer extends React.Component {
}
const classes = classNames({
- "mx_AppsDrawer": true,
- "mx_AppsDrawer_hidden": this.props.hide,
- "mx_AppsDrawer_fullWidth": apps.length < 2,
- "mx_AppsDrawer_minimised": !this.props.showApps,
+ mx_AppsDrawer: true,
+ mx_AppsDrawer_fullWidth: apps.length < 2,
+ mx_AppsDrawer_resizing: this.state.resizing,
+ mx_AppsDrawer_2apps: apps.length === 2,
+ mx_AppsDrawer_3apps: apps.length === 3,
});
return (
@@ -185,13 +255,20 @@ export default class AppsDrawer extends React.Component {
minHeight={100}
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
- className="mx_AppsContainer"
+ className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}
>
- { apps }
- { spinner }
+
+ { apps.map((app, i) => {
+ if (i < 1) return app;
+ return
+ apps.length / 2} />
+ { app }
+ ;
+ }) }
+
- { this._canUserModify() && addWidget }
+ { spinner }
);
}
@@ -208,14 +285,12 @@ const PersistentVResizer = ({
children,
}) => {
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
- const [resizing, setResizing] = useState(false);
return
{
- if (!resizing) setResizing(true);
resizeNotifier.startResizing();
}}
onResize={() => {
@@ -223,14 +298,11 @@ const PersistentVResizer = ({
}}
onResizeStop={(e, dir, ref, d) => {
setHeight(height + d.height);
- if (resizing) setResizing(false);
resizeNotifier.stopResizing();
}}
handleWrapperClass={handleWrapperClass}
handleClasses={{bottom: handleClass}}
- className={classNames(className, {
- mx_AppsDrawer_resizing: resizing,
- })}
+ className={className}
enable={{bottom: true}}
>
{ children }
diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js
index b7ed457a74..a088418d5e 100644
--- a/src/components/views/rooms/AuxPanel.js
+++ b/src/components/views/rooms/AuxPanel.js
@@ -37,7 +37,6 @@ export default class AuxPanel extends React.Component {
room: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
showApps: PropTypes.bool, // Render apps
- hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
// set to true to show the file drop target
draggingFile: PropTypes.bool,
@@ -54,7 +53,6 @@ export default class AuxPanel extends React.Component {
static defaultProps = {
showApps: true,
- hideAppsDrawer: false,
};
constructor(props) {
@@ -170,7 +168,6 @@ export default class AuxPanel extends React.Component {
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
- hide={this.props.hideAppsDrawer}
resizeNotifier={this.props.resizeNotifier}
/>;
}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index c2b1af2ddc..48ab6831d9 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -46,6 +46,7 @@ const eventTileTypes = {
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
+ 'm.call.reject': 'messages.TextualEvent',
};
const stateEventTileTypes = {
@@ -657,8 +658,7 @@ export default class EventTile extends React.Component {
// source tile when there's no regular tile for an event and also for
// replace relations (which otherwise would display as a confusing
// duplicate of the thing they are replacing).
- const useSource = !tileHandler || this.props.mxEvent.isRelation("m.replace");
- if (useSource && SettingsStore.getValue("showHiddenEventsInTimeline")) {
+ if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
tileHandler = "messages.ViewSourceEvent";
// Reuse info message avatar and sender profile styling
isInfoMessage = true;
diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js
index 8daea8dc17..b85dd2c8df 100644
--- a/src/components/views/rooms/ForwardMessage.js
+++ b/src/components/views/rooms/ForwardMessage.js
@@ -18,7 +18,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
-import dis from '../../../dispatcher/dispatcher';
import {Key} from '../../../Keyboard';
@@ -28,19 +27,10 @@ export default class ForwardMessage extends React.Component {
};
componentDidMount() {
- dis.dispatch({
- action: 'panel_disable',
- middleDisabled: true,
- });
-
document.addEventListener('keydown', this._onKeyDown);
}
componentWillUnmount() {
- dis.dispatch({
- action: 'panel_disable',
- middleDisabled: false,
- });
document.removeEventListener('keydown', this._onKeyDown);
}
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index a145770e3f..40eaf77272 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -37,6 +37,8 @@ import WidgetStore from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
+import { PlaceCallType } from "../../../CallHandler";
+import { CallState } from 'matrix-js-sdk/src/webrtc/call';
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@@ -53,7 +55,7 @@ function CallButton(props) {
const onVoiceCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
- type: "voice",
+ type: PlaceCallType.Voice,
room_id: props.roomId,
});
};
@@ -73,7 +75,7 @@ function VideoCallButton(props) {
const onCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
- type: ev.shiftKey ? "screensharing" : "video",
+ type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
room_id: props.roomId,
});
};
@@ -103,8 +105,11 @@ function HangupButton(props) {
if (!call) {
return;
}
+
+ const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
+
dis.dispatch({
- action: 'hangup',
+ action,
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId,
diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx
index 92e911067c..7725ce456e 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs.tsx
@@ -76,7 +76,7 @@ export default class RoomBreadcrumbs extends React.PureComponent
};
private viewRoom = (room: Room, index: number) => {
- Analytics.trackEvent("Breadcrumbs", "click_node", index);
+ Analytics.trackEvent("Breadcrumbs", "click_node", String(index));
defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId});
};
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 1a116838ac..8eb8276630 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -42,6 +42,8 @@ export default class RoomHeader extends React.Component {
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string,
+ onAppsClick: PropTypes.func,
+ appsShown: PropTypes.bool,
};
static defaultProps = {
@@ -230,6 +232,17 @@ export default class RoomHeader extends React.Component {
title={_t("Forget room")} />;
}
+ let appsButton;
+ if (this.props.onAppsClick) {
+ appsButton =
+ ;
+ }
+
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
@@ -243,6 +256,7 @@ export default class RoomHeader extends React.Component {
{ pinnedEventsButton }
{ forgetButton }
+ { appsButton }
{ searchButton }
;
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 92bbdfeacb..3a4d27e666 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -53,7 +53,6 @@ interface IProps {
onBlur: (ev: React.FocusEvent) => void;
onResize: () => void;
resizeNotifier: ResizeNotifier;
- collapsed: boolean;
isMinimized: boolean;
}
@@ -366,7 +365,7 @@ export default class RoomList extends React.PureComponent {
public render() {
let explorePrompt: JSX.Element;
- if (RoomListStore.instance.getFirstNameFilterCondition()) {
+ if (!this.props.isMinimized && RoomListStore.instance.getFirstNameFilterCondition()) {
explorePrompt =
{_t("Can't see what you’re looking for?")}
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index 4056f2fbd4..d93de47d6b 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -399,6 +399,7 @@ export default class RoomSublist extends React.Component {
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
+ this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change
};
private onTagSortChanged = async (sort: SortAlgorithm) => {
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 2faa0fea27..ae7ed48898 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -272,13 +272,10 @@ export default class Stickerpicker extends React.Component {
userId={MatrixClientPeg.get().credentials.userId}
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
waitForIframeLoad={true}
- show={true}
showMenubar={true}
onEditClick={this._launchManageIntegrations}
onDeleteClick={this._removeStickerpickerWidgets}
showTitle={false}
- showMinimise={true}
- showDelete={false}
showCancel={false}
showPopout={false}
onMinimiseClick={this._onHideStickersClick}
diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js
index fd6a62d73a..da11832cf5 100644
--- a/src/components/views/settings/IntegrationManager.js
+++ b/src/components/views/settings/IntegrationManager.js
@@ -42,6 +42,14 @@ export default class IntegrationManager extends React.Component {
loading: false,
};
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ errored: false,
+ };
+ }
+
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown);
@@ -66,6 +74,10 @@ export default class IntegrationManager extends React.Component {
}
};
+ onError = () => {
+ this.setState({ errored: true });
+ };
+
render() {
if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
@@ -77,7 +89,7 @@ export default class IntegrationManager extends React.Component {
);
}
- if (!this.props.connected) {
+ if (!this.props.connected || this.state.errored) {
return (
{_t("Cannot connect to integration manager")}
@@ -86,6 +98,6 @@ export default class IntegrationManager extends React.Component {
);
}
- return
;
+ return
;
}
}
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index 9acbece8b3..ca2b510f20 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -24,13 +24,14 @@ import dis from '../../../dispatcher/dispatcher';
import { ActionPayload } from '../../../dispatcher/payloads';
import PersistentApp from "../elements/PersistentApp";
import SettingsStore from "../../../settings/SettingsStore";
+import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
interface IProps {
}
interface IState {
roomId: string;
- activeCall: any;
+ activeCall: MatrixCall;
}
export default class CallPreview extends React.Component
{
@@ -84,7 +85,7 @@ export default class CallPreview extends React.Component {
if (call) {
dis.dispatch({
action: 'view_room',
- room_id: call.groupRoomId || call.roomId,
+ room_id: call.roomId,
});
}
};
@@ -93,7 +94,7 @@ export default class CallPreview extends React.Component {
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
const showCall = (
this.state.activeCall &&
- this.state.activeCall.call_state === 'connected' &&
+ this.state.activeCall.state === CallState.Connected &&
!callForRoom
);
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 2ab291ae86..3e1833a903 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import VideoView from "./VideoView";
import RoomAvatar from "../avatars/RoomAvatar";
import PulsedAvatar from '../avatars/PulsedAvatar';
+import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
interface IProps {
// js-sdk room object. If set, we will only show calls for the given
@@ -87,7 +88,7 @@ export default class CallView extends React.Component {
};
private showCall() {
- let call;
+ let call: MatrixCall;
if (this.props.room) {
const roomId = this.props.room.roomId;
@@ -120,7 +121,7 @@ export default class CallView extends React.Component {
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
}
- if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
+ if (call && call.type === "video" && call.state !== CallState.Ended && call.state !== CallState.Ringing) {
this.getVideoView().getLocalVideoElement().style.display = "block";
this.getVideoView().getRemoteVideoElement().style.display = "block";
} else {
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index 8e5d0f9e4a..355dff9ff6 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -25,6 +25,7 @@ import CallHandler from '../../../CallHandler';
import PulsedAvatar from '../avatars/PulsedAvatar';
import RoomAvatar from '../avatars/RoomAvatar';
import FormButton from '../elements/FormButton';
+import { CallState } from 'matrix-js-sdk/lib/webrtc/call';
interface IProps {
}
@@ -53,7 +54,7 @@ export default class IncomingCallBox extends React.Component {
switch (payload.action) {
case 'call_state': {
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
- if (call && call.call_state === 'ringing') {
+ if (call && call.state === CallState.Ringing) {
this.setState({
incomingCall: call,
});
@@ -77,7 +78,7 @@ export default class IncomingCallBox extends React.Component {
private onRejectClick: React.MouseEventHandler = (e) => {
e.stopPropagation();
dis.dispatch({
- action: 'hangup',
+ action: 'reject',
room_id: this.state.incomingCall.roomId,
});
};
diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts
new file mode 100644
index 0000000000..8fb0978375
--- /dev/null
+++ b/src/customisations/Security.ts
@@ -0,0 +1,81 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { IMatrixClientCreds } from "../MatrixClientPeg";
+import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast";
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function examineLoginResponse(
+ response: any,
+ credentials: IMatrixClientCreds,
+): void {
+ // E.g. add additional data to the persisted credentials
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function persistCredentials(
+ credentials: IMatrixClientCreds,
+): void {
+ // E.g. store any additional credential fields
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function createSecretStorageKey(): Uint8Array {
+ // E.g. generate or retrieve secret storage key somehow
+ return null;
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function getSecretStorageKey(): Uint8Array {
+ // E.g. retrieve secret storage key from some other place
+ return null;
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function catchAccessSecretStorageError(e: Error): void {
+ // E.g. notify the user in some way
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
+ // E.g. trigger some kind of setup
+ return false;
+}
+
+// This interface summarises all available customisation points and also marks
+// them all as optional. This allows customisers to only define and export the
+// customisations they need while still maintaining type safety.
+export interface ISecurityCustomisations {
+ examineLoginResponse?: (
+ response: any,
+ credentials: IMatrixClientCreds,
+ ) => void;
+ persistCredentials?: (
+ credentials: IMatrixClientCreds,
+ ) => void;
+ createSecretStorageKey?: () => Uint8Array,
+ getSecretStorageKey?: () => Uint8Array,
+ catchAccessSecretStorageError?: (
+ e: Error,
+ ) => void,
+ setupEncryptionNeeded?: (
+ kind: SetupEncryptionKind,
+ ) => boolean,
+}
+
+// A real customisation module will define and export one or more of the
+// customisation points that make up `ISecurityCustomisations`.
+export default {} as ISecurityCustomisations;
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 26d585b76e..6fb71df30d 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -94,14 +94,4 @@ export enum Action {
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
*/
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
-
- /**
- * Requests that the AppTile deletes the widget. Should be used with the AppTileActionPayload.
- */
- AppTileDelete = "appTile_delete",
-
- /**
- * Requests that the AppTile revokes the widget. Should be used with the AppTileActionPayload.
- */
- AppTileRevoke = "appTile_revoke",
}
diff --git a/src/email.js b/src/email.ts
similarity index 92%
rename from src/email.js
rename to src/email.ts
index 6e2ed69bb7..6642a51541 100644
--- a/src/email.js
+++ b/src/email.ts
@@ -16,6 +16,6 @@ limitations under the License.
const EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
-export function looksValid(email) {
+export function looksValid(email: string): boolean {
return EMAIL_ADDRESS_REGEX.test(email);
}
diff --git a/src/hooks/useLocalStorageState.ts b/src/hooks/useLocalStorageState.ts
index ce3b574f86..943db9ab4e 100644
--- a/src/hooks/useLocalStorageState.ts
+++ b/src/hooks/useLocalStorageState.ts
@@ -26,7 +26,7 @@ const getValue = (key: string, initialValue: T): T => {
};
// Hook behaving like useState but persisting the value to localStorage. Returns same as useState
-export const useLocalStorageState = (key: string, initialValue: T) => {
+export const useLocalStorageState = (key: string, initialValue: T): [T, Dispatch>] => {
const lsKey = "mx_" + key;
const [value, setValue] = useState(getValue(lsKey, initialValue));
diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json
index 32652669af..9cc53f5a9c 100644
--- a/src/i18n/strings/bg.json
+++ b/src/i18n/strings/bg.json
@@ -2305,5 +2305,22 @@
"You were uninvited": "Поканата към вас беше премахната",
"%(targetName)s was uninvited": "Поканата към %(targetName)s беше премахната",
"You were banned (%(reason)s)": "Бяхте блокирани (%(reason)s)",
- "%(targetName)s was banned (%(reason)s)": "%(targetName)s беше блокиран(а) (%(reason)s)"
+ "%(targetName)s was banned (%(reason)s)": "%(targetName)s беше блокиран(а) (%(reason)s)",
+ "%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
+ "* %(senderName)s %(emote)s": "%(senderName)s%(emote)s",
+ "The person who invited you already left the room, or their server is offline.": "Участникът който ви е поканил вече е напуснал стаята или техният сървър не е на линия.",
+ "The person who invited you already left the room.": "Участникът който ви покани вече напусна стаята.",
+ "Safeguard against losing access to encrypted messages & data": "Защитете се срещу загуба на достъп до криптирани съобшения и информация",
+ "Set up Secure Backup": "Конфигуриране на Защитен Архив",
+ "Unknown App": "Неизвестно приложение",
+ "Error leaving room": "Грешка при напускане на стаята",
+ "%(senderName)s declined the call.": "%(senderName)s отказа разговора.",
+ "(their device couldn't start the camera / microphone)": "(тяхното устройство не може да стартира камерата / микрофонът)",
+ "(connection failed)": "(връзката се разпадна)",
+ "Are you sure you want to cancel entering passphrase?": "Сигурни ли сте че желате да прекратите въвеждането на паролата?",
+ "This will end the conference for everyone. Continue?": "Това ще прекрати конферентният разговор за всички. Продължи?",
+ "End conference": "Прекрати конфетентният разговор",
+ "Call Declined": "Обаждането е отказано",
+ "The call could not be established": "Обаждането не може да бъде осъществено",
+ "The other party declined the call.": "Другата страна отказа обаждането."
}
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 734d3af75a..07eb46e40b 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2519,5 +2519,11 @@
"Video conference updated by %(senderName)s": "Videokonferenz wurde von %(senderName)s aktualisiert",
"Video conference started by %(senderName)s": "Videokonferenz wurde von %(senderName)s gestartet",
"Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert",
- "Failed to save your profile": "Profil speichern fehlgeschlagen"
+ "Failed to save your profile": "Profil speichern fehlgeschlagen",
+ "The operation could not be completed": "Die Operation konnte nicht abgeschlossen werden",
+ "Remove messages sent by others": "Nachrichten von anderen entfernen",
+ "Starting camera...": "Starte Kamera...",
+ "Call connecting...": "Verbinde den Anruf...",
+ "Calling...": "Rufe an...",
+ "Starting microphone...": "Starte Mikrofon..."
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b41a19aa21..8815b37d2a 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -35,8 +35,12 @@
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"Call Failed": "Call Failed",
- "Call Timeout": "Call Timeout",
+ "Call Declined": "Call Declined",
+ "The other party declined the call.": "The other party declined the call.",
"The remote side failed to pick up": "The remote side failed to pick up",
+ "The call could not be established": "The call could not be established",
+ "Answered Elsewhere": "Answered Elsewhere",
+ "The call was answered on another device.": "The call was answered on another device.",
"Call failed due to misconfigured server": "Call failed due to misconfigured server",
"Please ask the administrator of your homeserver (%(homeserverDomain)s
) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s
) to configure a TURN server in order for calls to work reliably.",
"Alternatively, you can try to use the public server at turn.matrix.org
, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org
, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
@@ -242,6 +246,9 @@
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s set the server ACLs for this room.",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s changed the server ACLs for this room.",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 All servers are banned from participating! This room can no longer be used.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
@@ -256,9 +263,13 @@
"(not supported by this browser)": "(not supported by this browser)",
"%(senderName)s answered the call.": "%(senderName)s answered the call.",
"(could not connect media)": "(could not connect media)",
+ "(connection failed)": "(connection failed)",
+ "(their device couldn't start the camera / microphone)": "(their device couldn't start the camera / microphone)",
+ "(an error occurred)": "(an error occurred)",
"(no answer)": "(no answer)",
"(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s ended the call.",
+ "%(senderName)s declined the call.": "%(senderName)s declined the call.",
"%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
"%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
@@ -1023,7 +1034,6 @@
"Remove %(phone)s?": "Remove %(phone)s?",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
"Phone Number": "Phone Number",
- "Add a widget": "Add a widget",
"Drop File Here": "Drop File Here",
"Drop file here to upload": "Drop file here to upload",
"This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
@@ -1105,6 +1115,8 @@
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
"Forget room": "Forget room",
+ "Hide Widgets": "Hide Widgets",
+ "Show Widgets": "Show Widgets",
"Search": "Search",
"Invites": "Invites",
"Favourites": "Favourites",
@@ -1270,8 +1282,11 @@
"Yours, or the other users’ session": "Yours, or the other users’ session",
"Members": "Members",
"Room Info": "Room Info",
+ "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
+ "Unpin": "Unpin",
+ "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
+ "Options": "Options",
"Widgets": "Widgets",
- "Unpin app": "Unpin app",
"Edit widgets, bridges & bots": "Edit widgets, bridges & bots",
"Add widgets, bridges & bots": "Add widgets, bridges & bots",
"Not encrypted": "Not encrypted",
@@ -1294,7 +1309,6 @@
"Invite": "Invite",
"Share Link to User": "Share Link to User",
"Direct message": "Direct message",
- "Options": "Options",
"Demote yourself?": "Demote yourself?",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
"Demote": "Demote",
@@ -1358,12 +1372,6 @@
"You cancelled verification.": "You cancelled verification.",
"Verification cancelled": "Verification cancelled",
"Compare emoji": "Compare emoji",
- "Take a picture": "Take a picture",
- "Remove for everyone": "Remove for everyone",
- "Remove for me": "Remove for me",
- "Edit": "Edit",
- "Pin to room": "Pin to room",
- "You can only pin 2 widgets at a time": "You can only pin 2 widgets at a time",
"Sunday": "Sunday",
"Monday": "Monday",
"Tuesday": "Tuesday",
@@ -1382,6 +1390,7 @@
"Error decrypting audio": "Error decrypting audio",
"React": "React",
"Reply": "Reply",
+ "Edit": "Edit",
"Message Actions": "Message Actions",
"Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment",
@@ -1474,15 +1483,7 @@
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
"Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.",
- "Delete Widget": "Delete Widget",
- "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
- "Delete widget": "Delete widget",
- "Failed to remove widget": "Failed to remove widget",
- "An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
- "Minimize widget": "Minimize widget",
- "Maximize widget": "Maximize widget",
"Popout widget": "Popout widget",
- "More options": "More options",
"Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files",
"Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages",
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
@@ -1760,6 +1761,8 @@
"Verify session": "Verify session",
"Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.",
"Message edits": "Message edits",
+ "Modal Widget": "Modal Widget",
+ "Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s",
"Your account is not secure": "Your account is not secure",
"Your password": "Your password",
"This session, or the other session": "This session, or the other session",
@@ -1915,9 +1918,14 @@
"Set status": "Set status",
"Set a new status...": "Set a new status...",
"View Community": "View Community",
- "Unpin": "Unpin",
- "Reload": "Reload",
- "Take picture": "Take picture",
+ "Take a picture": "Take a picture",
+ "Delete Widget": "Delete Widget",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
+ "Delete widget": "Delete widget",
+ "Remove for everyone": "Remove for everyone",
+ "Revoke permissions": "Revoke permissions",
+ "Move left": "Move left",
+ "Move right": "Move right",
"This room is public": "This room is public",
"Away": "Away",
"User Status": "User Status",
@@ -1992,6 +2000,8 @@
"You must join the room to see its files": "You must join the room to see its files",
"No files visible in this room": "No files visible in this room",
"Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.",
+ "Communities": "Communities",
+ "Create community": "Create community",
"HTML for your community's page
\n\n Use the long description to introduce new members to the community, or distribute\n some important links\n
\n\n You can even use 'img' tags\n
\n": "HTML for your community's page
\n\n Use the long description to introduce new members to the community, or distribute\n some important links\n
\n\n You can even use 'img' tags\n
\n",
"Add rooms to the community summary": "Add rooms to the community summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
@@ -2061,7 +2071,6 @@
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
- "Communities": "Communities",
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"You’re all caught up": "You’re all caught up",
@@ -2096,6 +2105,10 @@
"%(count)s of your messages have not been sent.|one": "Your message was not sent.",
"%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Resend all or cancel all now. You can also select individual messages to resend or cancel.",
"%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Resend message or cancel message now.",
+ "Calling...": "Calling...",
+ "Call connecting...": "Call connecting...",
+ "Starting camera...": "Starting camera...",
+ "Starting microphone...": "Starting microphone...",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?",
@@ -2113,7 +2126,6 @@
"Click to mute video": "Click to mute video",
"Click to unmute audio": "Click to unmute audio",
"Click to mute audio": "Click to mute audio",
- "Create community": "Create community",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 7d820b4fce..b4531b8ee7 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2524,5 +2524,19 @@
"Offline encrypted messaging using dehydrated devices": "Võrguühenduseta kasutamiseks mõeldud krüptitud sõnumid dehydrated teenuse abil",
"Remove messages sent by others": "Kustuta teiste saadetud sõnumid",
"Failed to save your profile": "Sinu profiili salvestamine ei õnnestunud",
- "The operation could not be completed": "Toimingut ei õnnestunud lõpetada"
+ "The operation could not be completed": "Toimingut ei õnnestunud lõpetada",
+ "Calling...": "Helistan...",
+ "Call connecting...": "Kõne on ühendamisel...",
+ "Starting camera...": "Käivitan kaamerat...",
+ "Starting microphone...": "Lülitan mikrofoni sisse...",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s muutis seda jututuba teenindavate koduserverite loendit.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s seadistas seda jututuba teenindavate koduserverite loendi.",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Kõikidel serveritel on keeld seda jututuba teenindada! Seega seda jututuba ei saa enam kasutada.",
+ "(an error occurred)": "(tekkis viga)",
+ "(their device couldn't start the camera / microphone)": "(teise osapoole seadmes ei õnnestunud sisse lülitada kaamerat või mikrofoni)",
+ "(connection failed)": "(ühendus ebaõnnestus)",
+ "The call could not be established": "Kõnet ei saa korraldada",
+ "%(senderName)s declined the call.": "%(senderName)s ei võtnud kõnet vastu.",
+ "The other party declined the call.": "Teine osapool ei võtnud kõnet vastu.",
+ "Call Declined": "Kõne on tagasilükatud"
}
diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json
index 6dd523cec6..5be1cdbe4a 100644
--- a/src/i18n/strings/fa.json
+++ b/src/i18n/strings/fa.json
@@ -36,7 +36,7 @@
"Unavailable": "غیرقابلدسترسی",
"View Decrypted Source": "دیدن منبع رمزگشایی شده",
"Failed to update keywords": "بهروزرسانی کلیدواژهها موفقیتآمیز نبود",
- "remove %(name)s from the directory.": "%(name)s را از فهرست گپها حذف کن",
+ "remove %(name)s from the directory.": "برداشتن %(name)s از فهرست گپها.",
"Please set a password!": "لطفا یک پسورد اختیار کنید!",
"powered by Matrix": "قدرتیافته از ماتریکس",
"You have successfully set a password!": "شما با موفقیت رمزتان را انتخاب کردید!",
@@ -149,5 +149,7 @@
"Restart": "شروع دوباره",
"Upgrade your %(brand)s": "ارتقای %(brand)s تان",
"A new version of %(brand)s is available!": "نگارشی جدید از %(brand)s موجود است!",
- "Guest": "مهمان"
+ "Guest": "مهمان",
+ "Confirm adding this email address by using Single Sign On to prove your identity.": "برای تأیید هویتتان، این نشانی رایانامه را با ورود یکپارچه تأیید کنید.",
+ "Click the button below to confirm adding this email address.": "برای تأیید افزودن این نشانی رایانامه، دکمهٔ زیر را بزنید."
}
diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index c6abfc0de7..d7aa91346e 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -525,9 +525,9 @@
"Unignored user": "Sallittu käyttäjä",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s tasolta %(fromPowerLevel)s tasolle %(toPowerLevel)s",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s muutti %(powerLevelDiffText)s:n oikeustasoa.",
- "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s muutti pienoisohjelmaa %(widgetName)s",
- "%(widgetName)s widget added by %(senderName)s": "%(senderName)s lisäsi pienoisohjelman %(widgetName)s",
- "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s poisti pienoisohjelman %(widgetName)s",
+ "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s muokkasi sovelmaa %(widgetName)s",
+ "%(widgetName)s widget added by %(senderName)s": "%(senderName)s lisäsi sovelman %(widgetName)s",
+ "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s poisti sovelman %(widgetName)s",
"Send": "Lähetä",
"Ongoing conference call%(supportedText)s.": "Menossa oleva ryhmäpuhelu %(supportedText)s.",
"%(duration)ss": "%(duration)s s",
@@ -549,7 +549,7 @@
"URL previews are disabled by default for participants in this room.": "URL-esikatselut ovat oletuksena pois päältä tämän huoneen jäsenillä.",
"Token incorrect": "Väärä tunniste",
"Something went wrong when trying to get your communities.": "Jokin meni pieleen yhteisöjäsi haettaessa.",
- "Delete Widget": "Poista pienoisohjelma",
+ "Delete Widget": "Poista sovelma",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s liittyivät",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s liittyi %(count)s kertaa",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s liittyi",
@@ -591,7 +591,7 @@
"expand": "laajenna",
"collapse": "supista",
"Display your community flair in rooms configured to show it.": "Näytä yhteisötyylisi huoneissa joissa ominaisuus on päällä.",
- "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Pienoisohjelman poistaminen poistaa sen kaikilta huoneen käyttäjiltä. Oletko varma että haluat poistaa pienoisohjelman?",
+ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Sovelman poistaminen poistaa sen kaikilta huoneen käyttäjiltä. Haluatko varmasti poistaa tämän sovelman?",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s liittyivät %(count)s kertaa",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s vaihtoivat nimensä %(count)s kertaa",
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s vaihtoivat nimensä",
@@ -743,7 +743,7 @@
"Every page you use in the app": "Jokainen sivu, jota käytät sovelluksessa",
"e.g. ": "esim. ",
"Your device resolution": "Laitteesi näytön tarkkuus",
- "You do not have permission to start a conference call in this room": "Sinulla ei ole oikeutta aloittaa konferenssipuhelua tässä huoneessa",
+ "You do not have permission to start a conference call in this room": "Sinulla ei ole oikeutta aloittaa ryhmäpuhelua tässä huoneessa",
"Upgrades a room to a new version": "Päivittää huoneen uuteen versioon",
"Gets or sets the room topic": "Hakee tai asettaa huoneen aiheen",
"This room has no topic.": "Tässä huoneessa ei ole aihetta.",
@@ -1096,7 +1096,7 @@
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Huoneen tyylin päivittämisessä tapahtui virhe. Palvelin ei välttämättä salli sitä tai kyseessä on tilapäinen virhe.",
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Salatuissa huoneissa, kuten tässä, osoitteiden esikatselut ovat oletuksena pois käytöstä, jotta kotipalvelimesi (missä osoitteiden esikatselut luodaan) ei voi kerätä tietoa siitä, mitä linkkejä näet tässä huoneessa.",
"Failed to remove widget": "Sovelman poisto epäonnistui",
- "An error ocurred whilst trying to remove the widget from the room": "Sovelman poistossa huoneesta tapahtui virhe",
+ "An error ocurred whilst trying to remove the widget from the room": "Poistaessa sovelmaa huoneesta tapahtui virhe",
"Minimize apps": "Pienennä sovellukset",
"Popout widget": "Avaa sovelma omassa ikkunassaan",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Lisää ”¯\\_(ツ)_/¯” viestin alkuun",
@@ -2222,5 +2222,27 @@
"We’re excited to announce Riot is now Element!": "Meillä on ilo ilmoittaa, että Riot on nyt Element!",
"Learn more at element.io/previously-riot": "Lue lisää osoitteessa element.io/previously-riot",
"Security & privacy": "Tietoturva ja -suoja",
- "User menu": "Käyttäjän valikko"
+ "User menu": "Käyttäjän valikko",
+ "Video conference started by %(senderName)s": "%(senderName)s aloitti videopuhelun",
+ "Video conference updated by %(senderName)s": "%(senderName)s muokkasi videopuhelua",
+ "Video conference ended by %(senderName)s": "%(senderName)s päätti videopuhelun",
+ "Join the conference from the room information card on the right": "Liity ryhmäpuheluun oikealla olevasta huoneen tiedoista",
+ "Join the conference at the top of this room": "Liity ryhmäpuheluun huoneen ylälaidassa",
+ "This will end the conference for everyone. Continue?": "Tämä päättää ryhmäpuhelun kaikilta. Jatka?",
+ "End conference": "Päätä ryhmäpuhelu",
+ "Wrong Recovery Key": "Väärä palautusavain",
+ "Wrong file type": "Väärä tiedostotyyppi",
+ "Please provide a room address": "Anna huoneen osoite",
+ "Room address": "Huoneen osoite",
+ "Message deleted on %(date)s": "Viesti poistettu %(date)s",
+ "Show %(count)s more|one": "Näytä %(count)s lisää",
+ "Show %(count)s more|other": "Näytä %(count)s lisää",
+ "Mod": "Moderaattori",
+ "Read Marker off-screen lifetime (ms)": "Viestin luetuksi merkkaamisen kesto, kun Element ei ole näkyvissä (ms)",
+ "Maximize widget": "Suurenna sovelma",
+ "Minimize widget": "Pienennä sovelma",
+ "You can only pin 2 widgets at a time": "Vain kaksi sovelmaa voi olla kiinnitettynä samaan aikaan",
+ "Add widgets, bridges & bots": "Lisää sovelmia, siltoja ja botteja",
+ "Edit widgets, bridges & bots": "Muokkaa sovelmia, siltoja ja botteja",
+ "Widgets": "Sovelmat"
}
diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 0dc3b770dd..e32874dd82 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -163,7 +163,7 @@
"Current password": "Contrasinal actual",
"Password": "Contrasinal",
"New Password": "Novo contrasinal",
- "Confirm password": "Confirme o contrasinal",
+ "Confirm password": "Confirma o contrasinal",
"Change Password": "Cambiar contrasinal",
"Authentication": "Autenticación",
"Last seen": "Visto por última vez",
@@ -424,7 +424,7 @@
"email address": "enderezo de correo",
"Try using one of the following valid address types: %(validTypesList)s.": "Intentar utilizar algún dos seguintes tipos de enderezo válidos: %(validTypesList)s.",
"You have entered an invalid address.": "Introduciu un enderezo non válido.",
- "Confirm Removal": "Confirme a retirada",
+ "Confirm Removal": "Confirma a retirada",
"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.": "Estás certa de que queres quitar (eliminar) este evento? Debes saber que se eliminas un nome de sala ou cambias o asunto, poderías desfacer o cambio.",
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Os ID de comunidade só poden conter caracteres a-z, 0-9, or '=_-./'",
"Community IDs cannot be empty.": "O ID de comunidade non pode quedar baldeiro.",
@@ -614,7 +614,7 @@
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Este proceso permíteche exportar a un ficheiro local as chaves para as mensaxes que recibiches en salas cifradas. Após poderás importar as chaves noutro cliente Matrix no futuro, así o cliente poderá descifrar esas mensaxes.",
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "O ficheiro exportado permitiralle a calquera que poida lelo descifrar e cifrar mensaxes que ti ves, así que deberías ter coidado e gardalo de xeito seguro. Para axudarche, deberías escribir unha frase de paso aquí abaixo que será usada para cifrar os datos exportados. Só será posible importar os datos utilizando a mesma frase de paso.",
"Enter passphrase": "Introduza a frase de paso",
- "Confirm passphrase": "Confirme a frase de paso",
+ "Confirm passphrase": "Confirma a frase de paso",
"Export": "Exportar",
"Import room keys": "Importar chaves de sala",
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Este proceso permíteche importar chaves de cifrado que exportaches doutro cliente Matrix. Así poderás descifrar calquera mensaxe que o outro cliente puidese cifrar.",
@@ -1887,7 +1887,7 @@
"Power level": "Nivel de permisos",
"Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verifica este dispositivo para marcalo como confiable. Confiando neste dispositivo permite que ti e outras usuarias estedes máis tranquilas ao utilizar mensaxes cifradas.",
"Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Ao verificar este dispositivo marcaralo como confiable, e as usuarias que confiaron en ti tamén confiarán nel.",
- "Waiting for partner to confirm...": "Agardando a que o compañeiro confirme...",
+ "Waiting for partner to confirm...": "Agardando a que o contacto confirme...",
"Incoming Verification Request": "Solicitude entrante de verificación",
"Integrations are disabled": "As Integracións están desactivadas",
"Enable 'Manage Integrations' in Settings to do this.": "Activa 'Xestionar Integracións' nos Axustes para facer esto.",
@@ -2523,5 +2523,19 @@
"Ignored attempt to disable encryption": "Intento ignorado de desactivar o cifrado",
"Failed to save your profile": "Non se gardaron os cambios",
"The operation could not be completed": "Non se puido realizar a acción",
- "Remove messages sent by others": "Eliminar mensaxes enviadas por outras"
+ "Remove messages sent by others": "Eliminar mensaxes enviadas por outras",
+ "Calling...": "Chamando...",
+ "Call connecting...": "Conectando a chamada...",
+ "Starting camera...": "Iniciando a cámara...",
+ "Starting microphone...": "Iniciando o micrófono...",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Tódolos servidores están prohibidos! Esta sala xa non pode ser utilizada.",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s cambiou ACLs de servidor para esta sala.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s estableceu ACLs de servidor para esta sala.",
+ "%(senderName)s declined the call.": "%(senderName)s rexeitou a chamada.",
+ "(an error occurred)": "(algo fallou)",
+ "(their device couldn't start the camera / microphone)": "(o dispositivo deles non puido iniciar a cámara / micrófono)",
+ "(connection failed)": "(fallou a conexión)",
+ "The call could not be established": "Non se puido establecer a chamada",
+ "The other party declined the call.": "A outra persoa rexeitou a chamada.",
+ "Call Declined": "Chamada rexeitada"
}
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 0e76c1a97d..e7735dcf79 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2524,5 +2524,19 @@
"Video conference started by %(senderName)s": "A videókonferenciát elindította: %(senderName)s",
"Failed to save your profile": "A profilodat nem sikerült elmenteni",
"The operation could not be completed": "A műveletet nem lehetett befejezni",
- "Remove messages sent by others": "Mások által küldött üzenetek törlése"
+ "Remove messages sent by others": "Mások által küldött üzenetek törlése",
+ "Starting microphone...": "Mikrofon bekapcsolása…",
+ "Starting camera...": "Kamera bekapcsolása…",
+ "Call connecting...": "Híváshoz csatlakozás…",
+ "Calling...": "Hívás…",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Minden szerver ki van tiltva! Ezt a szobát nem lehet többet használni.",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s megváltoztatta a jogosultságokat a szobában.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s beállította a jogosultságokat a szobában.",
+ "%(senderName)s declined the call.": "%(senderName)s visszautasította a hívást.",
+ "(an error occurred)": "(hiba történt)",
+ "(their device couldn't start the camera / microphone)": "(az ő eszköze nem tudja a kamerát / mikrofont használni)",
+ "(connection failed)": "(kapcsolódás sikertelen)",
+ "The call could not be established": "A hívás kapcsolatot nem lehet felépíteni",
+ "The other party declined the call.": "A másik fél elutasította a hívást.",
+ "Call Declined": "Hívás elutasítva"
}
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 83adb8c173..d6f7cc714e 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -805,7 +805,7 @@
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Per continuare a usare l'homeserver %(homeserverDomain)s devi leggere e accettare i nostri termini e condizioni.",
"Review terms and conditions": "Leggi i termini e condizioni",
"Muted Users": "Utenti silenziati",
- "Message Pinning": "Messaggi appuntati",
+ "Message Pinning": "Ancoraggio messaggi",
"Mirror local video feed": "Feed video dai ripetitori locali",
"Replying": "Rispondere",
"Popout widget": "Oggetto a comparsa",
@@ -2523,5 +2523,22 @@
"Video conference started by %(senderName)s": "Conferenza video iniziata da %(senderName)s",
"End conference": "Termina conferenza",
"This will end the conference for everyone. Continue?": "Verrà terminata la conferenza per tutti. Continuare?",
- "Ignored attempt to disable encryption": "Tentativo di disattivare la crittografia ignorato"
+ "Ignored attempt to disable encryption": "Tentativo di disattivare la crittografia ignorato",
+ "Failed to save your profile": "Salvataggio del profilo fallito",
+ "The operation could not be completed": "Impossibile completare l'operazione",
+ "Remove messages sent by others": "Rimuovi i messaggi inviati dagli altri",
+ "Calling...": "Chiamata in corso...",
+ "Call connecting...": "In connessione...",
+ "Starting camera...": "Avvio fotocamera...",
+ "Starting microphone...": "Avvio microfono...",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Tutti i server sono banditi dalla partecipazione! Questa stanza non può più essere usata.",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s ha cambiato le ACL del server per questa stanza.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s ha impostato le ACL del server per questa stanza.",
+ "%(senderName)s declined the call.": "%(senderName)s ha rifiutato la chiamata.",
+ "(an error occurred)": "(si è verificato un errore)",
+ "(their device couldn't start the camera / microphone)": "(il suo dispositivo non ha potuto avviare la fotocamera / il microfono)",
+ "(connection failed)": "(connessione fallita)",
+ "The call could not be established": "Impossibile stabilire la chiamata",
+ "The other party declined the call.": "Il destinatario ha rifiutato la chiamata.",
+ "Call Declined": "Chiamata rifiutata"
}
diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index ca1cf0a200..bab5b35125 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -1423,5 +1423,15 @@
"Upload files": "ファイルのアップロード",
"Upload all": "全てアップロード",
"No files visible in this room": "この部屋にファイルはありません",
- "Attach files from chat or just drag and drop them anywhere in a room.": "チャットでファイルを添付するか、部屋のどこかにドラッグ&ドロップするとファイルを追加できます。"
+ "Attach files from chat or just drag and drop them anywhere in a room.": "チャットでファイルを添付するか、部屋のどこかにドラッグ&ドロップするとファイルを追加できます。",
+ "Add widgets, bridges & bots": "ウィジェット、ブリッジ、ボットの追加",
+ "Widgets": "ウィジェット",
+ "Cross-signing is ready for use.": "クロス署名の使用準備が完了しています。",
+ "Secure Backup": "セキュアバックアップ",
+ "Set up Secure Backup": "セキュアバックアップのセットアップ",
+ "Restart": "再起動",
+ "Go back": "戻る",
+ "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "重複した issue の報告が発生しないようにするため、まず既存の issue を確認してあなたが行おうとしているのと同様の報告が見つかった場合はその issue を +1 してください。見つからなかった場合は、新しい issue を作成して報告を行ってください。",
+ "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "バグが発生したり、共有したいフィードバックがある場合は、GitHub でお知らせください。",
+ "Report bugs & give feedback": "バグ報告とフィードバック"
}
diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json
index 3b9b08ca6a..7a9261f25c 100644
--- a/src/i18n/strings/kab.json
+++ b/src/i18n/strings/kab.json
@@ -2438,5 +2438,16 @@
"There was an error updating your community. The server is unable to process your request.": "Tella-d tuccḍa deg uleqqem n temɣiwent-ik•im. Aqeddac ur izmir ara ad isesfer asuter.",
"Update community": "Leqqem tamɣiwent",
"May include members not in %(communityName)s": "Yezmer ad d-isseddu iɛeggalen ur nelli deg %(communityName)s",
- "Start a conversation with someone using their name, username (like ) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.": "Bdu adiwenni akked ḥedd s useqdec n yisem-is, isem uffir (am ) neɣ tansa imayl. Aya ur ten-iecced ara ɣer %(communityName)s. Akked ad d-tnecdeḍ yiwen ɣer %(communityName)s sit ɣef da."
+ "Start a conversation with someone using their name, username (like ) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.": "Bdu adiwenni akked ḥedd s useqdec n yisem-is, isem uffir (am ) neɣ tansa imayl. Aya ur ten-iecced ara ɣer %(communityName)s. Akked ad d-tnecdeḍ yiwen ɣer %(communityName)s sit ɣef da.",
+ "not found in storage": "Ulac-it deg uklas",
+ "Failed to save your profile": "Yecceḍ usekles n umaɣnu-ik•im",
+ "The operation could not be completed": "Tamahilt ur tezmir ara ad tettwasmed",
+ "Backup key cached:": "Tasarut n ukles tettwaffer:",
+ "Secret storage:": "Aklas uffir:",
+ "Remove messages sent by others": "Kkes iznan i uznen wiyaḍ",
+ "%(count)s results|one": "%(count)s n ugmuḍ",
+ "Widgets": "Iwiǧiten",
+ "Unpin app": "Serreḥ i usnas",
+ "Pin to room": "Sentu deg texxamt",
+ "You can only pin 2 widgets at a time": "Tzemreḍ ad tsentuḍ 2 kan n yiwiǧiten ɣef tikkelt"
}
diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 8eee0c0909..f7dc067a1a 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -10,7 +10,7 @@
"Banned users": "Usuários banidos",
"Bans user with given id": "Bane o usuário com o ID indicado",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s alterou a descrição para \"%(topic)s\".",
- "Changes your display nickname": "Alterar seu nome e sobrenome",
+ "Changes your display nickname": "Altera o seu nome e sobrenome",
"Click here to fix": "Clique aqui para resolver isso",
"Commands": "Comandos",
"Confirm password": "Confirme a nova senha",
@@ -20,7 +20,7 @@
"Current password": "Senha atual",
"Deactivate Account": "Desativar minha conta",
"Default": "Padrão",
- "Deops user with given id": "Retirar nível de moderador do usuário com o identificador informado",
+ "Deops user with given id": "Retira o nível de moderador do usuário com o ID informado",
"Displays action": "Visualizar atividades",
"Emoji": "Emoji",
"Error": "Erro",
@@ -70,7 +70,7 @@
"Return to login screen": "Retornar à tela de login",
"Room Colour": "Cores da sala",
"Rooms": "Salas",
- "Searches DuckDuckGo for results": "Buscar por resultados no buscador DuckDuckGo",
+ "Searches DuckDuckGo for results": "Buscar resultados no DuckDuckGo",
"Send Reset Email": "Enviar e-mail para redefinição de senha",
"Server may be unavailable, overloaded, or you hit a bug.": "O servidor pode estar indisponível ou sobrecarregado, ou então você encontrou uma falha no sistema.",
"Session ID": "Identificador de sessão",
@@ -168,7 +168,7 @@
"The remote side failed to pick up": "A pessoa não atendeu a chamada",
"This room is not recognised.": "Esta sala não é reconhecida.",
"This phone number is already in use": "Este número de telefone já está em uso",
- "To use it, just wait for autocomplete results to load and tab through them.": "Para usar este recurso, aguarde o carregamento dos resultados de autocompletar e então escolha entre as opções.",
+ "To use it, just wait for autocomplete results to load and tab through them.": "Para usar este recurso, aguarde o carregamento dos resultados de preenchimento automático, e então escolha dentre as opções.",
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s removeu o banimento de %(targetName)s.",
"Unable to capture screen": "Não foi possível capturar a imagem da tela",
"Unable to enable Notifications": "Não foi possível ativar as notificações",
@@ -414,7 +414,7 @@
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s de %(monthName)s de %(fullYear)s",
"Who would you like to add to this community?": "Quem você gostaria de adicionar a esta comunidade?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Atenção: qualquer pessoa que você adicionar a esta comunidade estará publicamente visível para todas as pessoas que conheçam o ID da comunidade",
- "Invite new community members": "Convidar novos participantes para a comunidade",
+ "Invite new community members": "Convidar novos integrantes para a comunidade",
"Which rooms would you like to add to this community?": "Quais salas você quer adicionar a esta comunidade?",
"Show these rooms to non-members on the community page and room list?": "Exibir estas salas para não participantes na página da comunidade e na lista de salas?",
"Unable to create widget.": "Não foi possível criar o widget.",
@@ -632,9 +632,9 @@
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Um e-mail foi enviado para %(emailAddress)s. Após clicar no link contido no e-mail, clique abaixo.",
"Please note you are logging into the %(hs)s server, not matrix.org.": "Note que você está se conectando ao servidor %(hs)s, e não ao servidor matrix.org.",
"This homeserver doesn't offer any login flows which are supported by this client.": "Este servidor de base (homeserver) não oferece fluxos de login que funcionem neste cliente.",
- "Define the power level of a user": "Definir o nível de permissões de um(a) usuário(a)",
- "Ignores a user, hiding their messages from you": "Bloquear um usuário, esconderá as mensagens dele de você",
- "Stops ignoring a user, showing their messages going forward": "Desbloquear um usuário, exibe suas mensagens daqui para frente",
+ "Define the power level of a user": "Define o nível de permissões de um usuário",
+ "Ignores a user, hiding their messages from you": "Bloqueia um usuário, escondendo as mensagens dele de você",
+ "Stops ignoring a user, showing their messages going forward": "Desbloqueia um usuário, exibindo as mensagens dele daqui para frente",
"Notify the whole room": "Notifica a sala inteira",
"Room Notification": "Notificação da sala",
"Failed to set direct chat tag": "Falha ao definir esta conversa como direta",
@@ -708,13 +708,13 @@
"Failed to set Direct Message status of room": "Falha em definir a descrição da conversa",
"Monday": "Segunda-feira",
"All messages (noisy)": "Todas as mensagens (com som)",
- "Enable them now": "Ativá-los agora",
+ "Enable them now": "Ativar agora",
"Toolbox": "Ferramentas",
"Collecting logs": "Coletando logs",
"You must specify an event type!": "Você precisa especificar um tipo do evento!",
"(HTTP status %(httpStatus)s)": "(Status HTTP %(httpStatus)s)",
"Invite to this room": "Convidar para esta sala",
- "Send logs": "Enviar registros",
+ "Send logs": "Enviar relatórios",
"All messages": "Todas as mensagens novas",
"Call invitation": "Recebendo chamada",
"Downloading update...": "Baixando atualização...",
@@ -821,8 +821,8 @@
"Messages containing @room": "Mensagens contendo @room",
"Encrypted messages in one-to-one chats": "Mensagens criptografadas em conversas individuais",
"Encrypted messages in group chats": "Mensagens criptografadas em salas",
- "Delete Backup": "Deletar Backup",
- "Unable to load key backup status": "Não é possível carregar o status da chave de backup",
+ "Delete Backup": "Remover backup",
+ "Unable to load key backup status": "Não foi possível carregar o status do backup da chave",
"Backup version: ": "Versão do Backup: ",
"Algorithm: ": "Algoritmo: ",
"This event could not be displayed": "Este evento não pôde ser exibido",
@@ -860,12 +860,12 @@
"An error ocurred whilst trying to remove the widget from the room": "Ocorreu um erro ao tentar remover o widget da sala",
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Não é possível carregar o evento que foi respondido, ele não existe ou você não tem permissão para visualizá-lo.",
"That doesn't look like a valid email address": "Este não parece ser um endereço de e-mail válido",
- "Preparing to send logs": "Preparando para enviar registros",
- "Logs sent": "Registros enviados",
- "Failed to send logs: ": "Falha ao enviar registros:· ",
- "Submit debug logs": "Submeter registros de depuração",
- "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Os registros de depuração contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou aliases das salas ou comunidades que você visitou e os nomes de usuários de outros usuários. Eles não contêm mensagens.",
- "Before submitting logs, you must create a GitHub issue to describe your problem.": "Antes de enviar os registros, você deve criar um bilhete de erro no GitHub para descrever seu problema.",
+ "Preparing to send logs": "Preparando para enviar relatórios",
+ "Logs sent": "Relatórios enviados",
+ "Failed to send logs: ": "Falha ao enviar os relatórios:· ",
+ "Submit debug logs": "Enviar relatórios de erros",
+ "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Os relatórios de erros contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou nomes das salas ou comunidades que você visitou e os nomes de usuários de seus contatos. Eles não contêm mensagens.",
+ "Before submitting logs, you must create a GitHub issue to describe your problem.": "Antes de enviar os relatórios, você deve criar um bilhete de erro no GitHub para descrever seu problema.",
"Unable to load commit detail: %(msg)s": "Não foi possível carregar os detalhes do envio: %(msg)s",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Para evitar perder seu histórico de bate-papo, você precisa exportar as chaves da sua sala antes de se desconectar. Quando entrar novamente, você precisará usar a versão mais atual do %(brand)s",
"Incompatible Database": "Banco de dados incompatível",
@@ -888,7 +888,7 @@
"Put a link back to the old room at the start of the new room so people can see old messages": "Colocar um link para a sala antiga no começo da sala nova de modo que as pessoas possam ver mensagens antigas",
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Você já usou o %(brand)s em %(host)s com o carregamento Lazy de participantes ativado. Nesta versão, o carregamento Lazy está desativado. Como o cache local não é compatível entre essas duas configurações, o %(brand)s precisa ressincronizar sua conta.",
"If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Se a outra versão do %(brand)s ainda estiver aberta em outra aba, por favor, feche-a pois usar o %(brand)s no mesmo host com o carregamento Lazy ativado e desativado simultaneamente causará problemas.",
- "Update any local room aliases to point to the new room": "Atualize todos os aliases da sala local para apontar para a nova sala",
+ "Update any local room aliases to point to the new room": "Atualize todos os nomes locais da sala para apontar para a nova sala",
"Clear Storage and Sign Out": "Limpar armazenamento e sair",
"Refresh": "Recarregar",
"We encountered an error trying to restore your previous session.": "Encontramos um erro ao tentar restaurar sua sessão anterior.",
@@ -901,8 +901,8 @@
"Share Room Message": "Compartilhar Mensagem da Sala",
"Link to selected message": "Link da mensagem selecionada",
"COPY": "COPIAR",
- "Unable to load backup status": "Não é possível carregar o status do backup",
- "Unable to restore backup": "Não é possível restaurar o backup",
+ "Unable to load backup status": "Não foi possível carregar o status do backup",
+ "Unable to restore backup": "Não foi possível restaurar o backup",
"No backup found!": "Nenhum backup encontrado!",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Acesse seu histórico de mensagens seguras e configure mensagens seguras digitando sua frase secreta de recuperação.",
"Next": "Próximo",
@@ -912,7 +912,7 @@
"Access your secure message history and set up secure messaging by entering your recovery key.": "Acesse seu histórico seguro de mensagens e configure mensagens seguras inserindo sua chave de recuperação.",
"Share Message": "Compartilhar Mensagem",
"Popout widget": "Widget Popout",
- "Send Logs": "Enviar registros",
+ "Send Logs": "Enviar relatórios",
"Failed to decrypt %(failedCount)s sessions!": "Falha ao descriptografar as sessões de %(failedCount)s!",
"Set a new status...": "Definir um novo status ...",
"Collapse Reply Thread": "Recolher grupo de respostas",
@@ -933,7 +933,7 @@
"You can't send any messages until you review and agree to our terms and conditions.": "Você não pode enviar nenhuma mensagem até revisar e concordar com nossos termos e condições.",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Sua mensagem não foi enviada porque este homeserver atingiu seu Limite de usuário ativo mensal. Por favor, entre em contato com o seu administrador de serviços para continuar usando o serviço.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Sua mensagem não foi enviada porque este servidor local excedeu o limite de recursos. Por favor, entre em contato com o seu administrador de serviços para continuar usando o serviço.",
- "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se você enviou um bug por meio do GitHub, os registros de depuração podem nos ajudar a rastrear o problema. Os registros de depuração contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou apelidos das salas ou comunidades que você visitou e os nomes de usuários de outros usuários. Eles não contêm mensagens.",
+ "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se você informou um erro por meio do GitHub, os relatórios de erros podem nos ajudar a rastrear o problema. Os relatórios de erros contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou apelidos das salas ou comunidades que você visitou e os nomes de usuários de seus contatos. Eles não contêm mensagens.",
"Legal": "Legal",
"No Audio Outputs detected": "Nenhuma caixa de som detectada",
"Audio Output": "Caixa de som",
@@ -951,12 +951,12 @@
"Save it on a USB key or backup drive": "Salve isto b> em uma chave USB ou unidade de backup",
"Copy it to your personal cloud storage": "Copie isto b> para seu armazenamento em nuvem pessoal",
"Set up Secure Message Recovery": "Configurar Recuperação Segura de Mensagens",
- "Unable to create key backup": "Não é possível criar backup de chave",
+ "Unable to create key backup": "Não foi possível criar backup da chave",
"Retry": "Tentar novamente",
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Sem configurar a Recuperação Segura de Mensagens, você perderá seu histórico de mensagens seguras quando fizer logout.",
"If you don't want to set this up now, you can later in Settings.": "Se você não quiser configurá-lo agora, poderá fazê-lo posteriormente em Configurações.",
- "New Recovery Method": "Novo método de recuperação",
- "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se você não definiu o novo método de recuperação, um invasor pode estar tentando acessar sua conta. Altere a senha da sua conta e defina um novo método de recuperação imediatamente nas Configurações.",
+ "New Recovery Method": "Nova opção de recuperação",
+ "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se você não definiu a nova opção de recuperação, um invasor pode estar tentando acessar sua conta. Altere a senha da sua conta e defina uma nova opção de recuperação imediatamente nas Configurações.",
"Set up Secure Messages": "Configurar mensagens seguras",
"Go to Settings": "Ir para as configurações",
"Unrecognised address": "Endereço não reconhecido",
@@ -967,10 +967,10 @@
"Invite anyway and never warn me again": "Convide mesmo assim e nunca mais me avise",
"Invite anyway": "Convide mesmo assim",
"Whether or not you're logged in (we don't record your username)": "Se você está logado ou não (não gravamos seu nome de usuário)",
- "Upgrades a room to a new version": "Atualiza uma sala para uma nova versão",
- "Gets or sets the room topic": "Consultar ou definir a descrição da sala",
+ "Upgrades a room to a new version": "Atualiza a sala para uma nova versão",
+ "Gets or sets the room topic": "Consulta ou altera a descrição da sala",
"This room has no topic.": "Esta sala não tem descrição.",
- "Sets the room name": "Define o nome da sala",
+ "Sets the room name": "Altera o nome da sala",
"Group & filter rooms by custom tags (refresh to apply changes)": "Agrupar e filtrar salas por tags personalizadas (recarregue para aplicar as alterações)",
"Render simple counters in room header": "Renderizar contadores simples no cabeçalho da sala",
"Enable Emoji suggestions while typing": "Ativar sugestões de emojis ao digitar",
@@ -978,7 +978,7 @@
"Show join/leave messages (invites/kicks/bans unaffected)": "Mostrar mensagens de entrar/sair (não considera convites/remoções/banimentos)",
"Show avatar changes": "Mostrar alterações de foto de perfil",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "O arquivo '%(fileName)s' excede o limite de tamanho deste homeserver para uploads",
- "Changes your display nickname in the current room only": "Altera o seu nome e sobrenome apenas na sala atual",
+ "Changes your display nickname in the current room only": "Altera o seu nome e sobrenome apenas nesta sala",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s atualizou esta sala.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s tornou a sala pública para quem conhece o link.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s tornou a sala disponível apenas por convite.",
@@ -1005,7 +1005,7 @@
"You've successfully verified this user.": "Você confirmou este usuário com sucesso.",
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "As mensagens com este usuário estão protegidas com a criptografia de ponta a ponta e não podem ser lidas por terceiros.",
"Got It": "Ok, entendi",
- "Unable to find a supported verification method.": "Nenhum método de confirmação é suportado.",
+ "Unable to find a supported verification method.": "Nenhuma opção de confirmação é suportada.",
"Dog": "Cachorro",
"Cat": "Gato",
"Lion": "Leão",
@@ -1042,7 +1042,7 @@
"Glasses": "Óculos",
"Spanner": "Chave inglesa",
"Santa": "Papai-noel",
- "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Adiciona ¯ \\ _ (ツ) _ / ¯ no início de uma mensagem de texto simples",
+ "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Adiciona ¯ \\ _ (ツ) _ / ¯ a uma mensagem de texto",
"User %(userId)s is already in the room": "O usuário %(userId)s já está na sala",
"The user must be unbanned before they can be invited.": "O banimento do usuário precisa ser removido antes de ser convidado.",
"Show display name changes": "Mostrar alterações de nome e sobrenome",
@@ -1079,13 +1079,13 @@
"No": "Não",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Enviamos um e-mail para você confirmar seu endereço. Por favor, siga as instruções e clique no botão abaixo.",
"Email Address": "Endereço de e-mail",
- "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Você tem certeza? Você perderá suas mensagens criptografadas se não for feito o backup correto de suas chaves.",
+ "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Tem certeza? Você perderá suas mensagens criptografadas se não tiver feito o backup de suas chaves.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "As mensagens estão protegidas com a criptografia de ponta a ponta. Somente você e o(s) destinatário(s) têm as chaves para ler essas mensagens.",
- "Restore from Backup": "Restaurar do Backup",
+ "Restore from Backup": "Restaurar do backup",
"Back up your keys before signing out to avoid losing them.": "Faça o backup das suas chaves antes de sair, para evitar perdê-las.",
"Backing up %(sessionsRemaining)s keys...": "Fazendo o backup das chaves de %(sessionsRemaining)s...",
- "All keys backed up": "O Backup de todas as chaves foi realizado",
- "Start using Key Backup": "Comece a usar o Backup de chave",
+ "All keys backed up": "O backup de todas as chaves foi realizado",
+ "Start using Key Backup": "Comece a usar backup de chave",
"Add an email address to configure email notifications": "Adicione um endereço de e-mail para configurar notificações por e-mail",
"Unable to verify phone number.": "Não foi possível confirmar o número de telefone.",
"Verification code": "Código de confirmação",
@@ -1181,12 +1181,12 @@
"Messages": "Mensagens",
"Actions": "Ações",
"Other": "Outros",
- "Sends a message as plain text, without interpreting it as markdown": "Envia uma mensagem como texto simples, sem formatar o texto",
- "Sends a message as html, without interpreting it as markdown": "Envia uma mensagem como HTML, sem formatar o texto",
+ "Sends a message as plain text, without interpreting it as markdown": "Envia uma mensagem de texto sem formatação",
+ "Sends a message as html, without interpreting it as markdown": "Envia uma mensagem como HTML, sem formatação",
"You do not have the required permissions to use this command.": "Você não tem as permissões necessárias para usar este comando.",
"Error upgrading room": "Erro atualizando a sala",
"Double check that your server supports the room version chosen and try again.": "Verifique se seu servidor suporta a versão de sala escolhida e tente novamente.",
- "Changes the avatar of the current room": "Altera a foto da sala atual",
+ "Changes the avatar of the current room": "Altera a foto da sala",
"Changes your avatar in this current room only": "Altera a sua foto de perfil apenas nesta sala",
"Changes your avatar in all rooms": "Altera a sua foto de perfil em todas as salas",
"Failed to set topic": "Não foi possível definir a descrição",
@@ -1211,9 +1211,9 @@
"Sends the given emote coloured as a rainbow": "Envia o emoji colorido como um arco-íris",
"Displays list of commands with usages and descriptions": "Exibe a lista de comandos com usos e descrições",
"Displays information about a user": "Exibe informação sobre um usuário",
- "Send a bug report with logs": "Envia um relatório de erros com os logs",
+ "Send a bug report with logs": "Envia um relatório de erro",
"Opens chat with the given user": "Abre um chat com determinada pessoa",
- "Sends a message to the given user": "Envia uma mensagem com determinada pessoa",
+ "Sends a message to the given user": "Envia uma mensagem para determinada pessoa",
"%(senderName)s made no change.": "%(senderName)s não fez nenhuma alteração.",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s alterou o nome da sala de %(oldRoomName)s para %(newRoomName)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s adicionou os endereços alternativos %(addresses)s desta sala.",
@@ -1328,7 +1328,7 @@
"Try out new ways to ignore people (experimental)": "Tente novas maneiras de bloquear pessoas (experimental)",
"Support adding custom themes": "Permite adicionar temas personalizados",
"Enable advanced debugging for the room list": "Ativar a depuração avançada para a lista de salas",
- "Show info about bridges in room settings": "Exibir informações sobre bridges nas configurações das salas",
+ "Show info about bridges in room settings": "Exibir informações sobre integrações nas configurações das salas",
"Font size": "Tamanho da fonte",
"Use custom size": "Usar tamanho personalizado",
"Use a more compact ‘Modern’ layout": "Usar um layout mais compacto 'Moderno'",
@@ -1378,8 +1378,8 @@
"Decline (%(counter)s)": "Recusar (%(counter)s)",
"Accept to continue:": "Aceitar para continuar:",
"Upload": "Enviar",
- "This bridge was provisioned by .": "Esta ponte foi disponibilizada por .",
- "This bridge is managed by .": "Esta ponte é gerida por .",
+ "This bridge was provisioned by .": "Esta integração foi disponibilizada por .",
+ "This bridge is managed by .": "Esta integração é desenvolvida por .",
"Workspace: %(networkName)s": "Espaço de trabalho: %(networkName)s",
"Channel: %(channelName)s": "Canal: %(channelName)s",
"Show less": "Mostrar menos",
@@ -1432,23 +1432,23 @@
"Connecting to integration manager...": "Conectando ao gestor de integrações...",
"Cannot connect to integration manager": "Não foi possível conectar ao gerenciador de integrações",
"The integration manager is offline or it cannot reach your homeserver.": "Ou o gerenciador de integrações está desconectado, ou ele não conseguiu acessar o seu servidor.",
- "This session is backing up your keys. ": "Esta sessão está fazendo a cópia (backup) das suas chaves. ",
- "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Esta sessão não está fazendo cópia (backup) de suas chaves, mas você tem uma cópia existente que pode restaurar e adicionar para continuar.",
- "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Conecte esta sessão à cópia de segurança (backup) das chaves antes de fazer logout para evitar perder quaisquer chaves que possam estar apenas nesta sessão.",
- "Connect this session to Key Backup": "Conecte esta sessão à Cópia de Segurança (Backup) da Chave",
+ "This session is backing up your keys. ": "Esta sessão está fazendo backup das suas chaves. ",
+ "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Esta sessão não está fazendo backup de suas chaves, mas você tem um backup existente que pode restaurar para continuar.",
+ "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Autorize esta sessão a fazer o backup de chaves antes de se desconectar, para evitar perder chaves que possam estar apenas nesta sessão.",
+ "Connect this session to Key Backup": "Autorize esta sessão a fazer o backup de chaves",
"not stored": "não armazenado",
- "Backup has a valid signature from this user": "A cópia de segurança (backup) tem uma assinatura válida deste(a) usuário(a)",
- "Backup has a invalid signature from this user": "A cópia de segurança (backup) tem uma assinatura inválida deste(a) usuário(a)",
- "Backup has a signature from unknown user with ID %(deviceId)s": "A cópia de segurança tem uma assinatura de um usuário desconhecido com ID %(deviceId)s",
- "Backup has a signature from unknown session with ID %(deviceId)s": "A cópia de segurança tem uma assinatura de uma sessão desconhecida com ID %(deviceId)s",
- "Backup has a valid signature from this session": "A cópia de segurança (backup) tem uma assinatura válida desta sessão",
- "Backup has an invalid signature from this session": "A cópia de segurança (backup) tem uma assinatura inválida desta sessão",
- "Backup has a valid signature from verified session ": "A cópia de segurança (backup) tem uma assinatura válida da sessão confirmada ",
- "Backup has a valid signature from unverified session ": "A cópia de segurança tem uma assinatura válida de uma sessão não confirmada ",
- "Backup has an invalid signature from verified session ": "A cópia de segurança tem uma assinatura inválida de uma sessão confirmada ",
- "Backup has an invalid signature from unverified session ": "A cópia de segurança (backup) tem uma assinatura inválida de uma sessão não confirmada ",
- "Backup is not signed by any of your sessions": "A cópia de segurança (backup) não foi assinada por nenhuma de suas sessões",
- "This backup is trusted because it has been restored on this session": "Esta cópia de segurança (backup) é confiável, pois foi restaurada nesta sessão",
+ "Backup has a valid signature from this user": "O backup tem uma assinatura válida deste usuário",
+ "Backup has a invalid signature from this user": "O backup tem uma assinatura inválida deste usuário",
+ "Backup has a signature from unknown user with ID %(deviceId)s": "O backup tem uma assinatura de um usuário desconhecido com ID %(deviceId)s",
+ "Backup has a signature from unknown session with ID %(deviceId)s": "O backup tem uma assinatura de uma sessão desconhecida com ID %(deviceId)s",
+ "Backup has a valid signature from this session": "O backup tem uma assinatura válida desta sessão",
+ "Backup has an invalid signature from this session": "O backup tem uma assinatura inválida desta sessão",
+ "Backup has a valid signature from verified session ": "O backup tem uma assinatura válida da sessão confirmada ",
+ "Backup has a valid signature from unverified session ": "O backup tem uma assinatura válida de uma sessão não confirmada ",
+ "Backup has an invalid signature from verified session ": "O backup tem uma assinatura inválida de uma sessão confirmada ",
+ "Backup has an invalid signature from unverified session ": "O backup tem uma assinatura inválida de uma sessão não confirmada ",
+ "Backup is not signed by any of your sessions": "O backup não foi assinado por nenhuma de suas sessões",
+ "This backup is trusted because it has been restored on this session": "Este backup é confiável, pois foi restaurado nesta sessão",
"Backup key stored: ": "Chave de segurança (backup) armazenada: ",
"Your keys are not being backed up from this session.": "Suas chaves não estão sendo copiadas desta sessão.",
"wait and try again later": "aguarde e tente novamente mais tarde",
@@ -1460,7 +1460,7 @@
"A session's public name is visible to people you communicate with": "O nome público de uma sessão é visível para as pessoas com quem você se comunica",
"Enable room encryption": "Ativar criptografia nesta sala",
"Enable encryption?": "Ativar criptografia?",
- "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Uma vez ativada, a criptografia da sala não poderá ser desativada. Mensagens enviadas em uma sala criptografada não podem ser lidas pelo servidor, apenas pelos participantes da sala. Ativar a criptografia poderá impedir que vários bots e bridges funcionem corretamente. Saiba mais sobre criptografia.",
+ "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Uma vez ativada, a criptografia da sala não poderá ser desativada. Mensagens enviadas em uma sala criptografada não podem ser lidas pelo servidor, apenas pelos participantes da sala. Ativar a criptografia poderá impedir que vários bots e integrações funcionem corretamente. Saiba mais sobre criptografia.",
"Encryption": "Criptografia",
"Once enabled, encryption cannot be disabled.": "Uma vez ativada, a criptografia não poderá ser desativada.",
"Encrypted": "Criptografada",
@@ -1514,7 +1514,7 @@
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Por favor, diga-nos o que aconteceu de errado ou, ainda melhor, crie um bilhete de erro no GitHub que descreva o problema.",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Apagar todos os dados desta sessão é uma ação permanente. Mensagens criptografadas serão perdidas, a não ser que as chaves delas tenham sido copiadas para o backup.",
"Set a room address to easily share your room with other people.": "Defina um endereço de sala para facilmente compartilhar sua sala com outras pessoas.",
- "You can’t disable this later. Bridges & most bots won’t work yet.": "Você não poderá desativar isso mais tarde. Pontes e a maioria dos bots não funcionarão.",
+ "You can’t disable this later. Bridges & most bots won’t work yet.": "Você não poderá desativar isso mais tarde. Integrações e a maioria dos bots não funcionarão.",
"Enable end-to-end encryption": "Ativar a criptografia de ponta a ponta",
"Create a public room": "Criar uma sala pública",
"Create a private room": "Criar uma sala privada",
@@ -1540,7 +1540,7 @@
"To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Para evitar a duplicação de registro de problemas, por favor veja os problemas existentes antes e adicione um +1, ou então crie um novo item se seu problema ainda não foi reportado.",
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reportar esta mensagem enviará o seu 'event ID' único para o/a administrador/a do seu Homeserver. Se as mensagens nesta sala são criptografadas, o/a administrador/a não conseguirá ler o texto da mensagem nem ver nenhuma imagem ou arquivo.",
"Sign out and remove encryption keys?": "Fazer logout e remover as chaves de criptografia?",
- "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Alguns dados de sessão, incluindo chaves de mensagens criptografadas, estão faltando. Faça logout e entre novamente para resolver isso, restaurando as chaves do backup.",
+ "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Alguns dados de sessão, incluindo chaves de mensagens criptografadas, estão faltando. Desconecte-se e entre novamente para resolver isso, o que restaurará as chaves do backup.",
"Verify other session": "Confirmar outra sessão",
"A widget would like to verify your identity": "Um widget deseja confirmar sua identidade",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Um widget localizado em %(widgetUrl)s deseja confirmar sua identidade. Permitindo isso, o widget poderá verificar sua ID de usuário, mas não poderá realizar nenhuma ação em seu nome.",
@@ -1553,15 +1553,15 @@
"Recovery key mismatch": "Chave de recuperação incorreta",
"Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "O backup não pôde ser descriptografado com esta chave de recuperação: por favor, verifique se você digitou a chave de recuperação correta.",
"Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "O backup não pôde ser descriptografado com esta frase de recuperação: por favor, verifique se você digitou a frase de recuperação correta.",
- "Warning: you should only set up key backup from a trusted computer.": "Atenção: você só deve configurar a cópia de segurança (backup) das chaves em um computador de sua confiança.",
+ "Warning: you should only set up key backup from a trusted computer.": "Atenção: você só deve configurar o backup de chave em um computador de sua confiança.",
"Enter recovery key": "Digite a chave de recuperação",
- "Warning: You should only set up key backup from a trusted computer.": "Atenção: Você só deve configurar a cópia de segurança (backup) das chaves em um computador de sua confiança.",
+ "Warning: You should only set up key backup from a trusted computer.": "Atenção: Você só deve configurar o backup de chave em um computador de sua confiança.",
"If you've forgotten your recovery key you can ": "Se você esqueceu sua chave de recuperação, pode ",
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Está faltando a chave pública do captcha no Servidor (homeserver). Por favor, reporte isso aos(às) administradores(as) do servidor.",
"Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "Entre com a localização do seu Servidor Matrix. Pode ser seu próprio domínio ou ser um subdomínio de element.io.",
"Create your Matrix account on %(serverName)s": "Criar sua conta Matrix em %(serverName)s",
"Create your Matrix account on ": "Crie sua conta Matrix em ",
- "Welcome to %(appName)s": "Bem-vinda/o ao %(appName)s",
+ "Welcome to %(appName)s": "Boas-vindas ao %(appName)s",
"Liberate your communication": "Liberte sua comunicação",
"Send a Direct Message": "Enviar uma mensagem",
"Explore Public Rooms": "Explorar salas públicas",
@@ -1571,7 +1571,7 @@
"%(creator)s created and configured the room.": "%(creator)s criou e configurou esta sala.",
"If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se você não conseguir encontrar a sala que está procurando, peça um convite para a sala ou Crie uma nova sala.",
"Verify this login": "Confirmar este login",
- "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Alterar a sua senha redefinirá todas as chaves de criptografia de ponta a ponta existentes em todas as suas sessões, tornando o histórico de mensagens criptografadas ilegível. Faça uma cópia (backup) das suas chaves, ou exporte as chaves de outra sessão antes de alterar a sua senha.",
+ "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Alterar a sua senha redefinirá todas as chaves de criptografia de ponta a ponta existentes em todas as suas sessões, tornando o histórico de mensagens criptografadas ilegível. Faça um backup das suas chaves, ou exporte as chaves de outra sessão antes de alterar a sua senha.",
"Create account": "Criar conta",
"Create your account": "Criar sua conta",
"Use Recovery Key or Passphrase": "Use a chave de recuperação, ou a frase de recuperação",
@@ -1583,17 +1583,17 @@
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Atenção: Seus dados pessoais (incluindo chaves de criptografia) ainda estão armazenados nesta sessão. Apague-os quando tiver finalizado esta sessão, ou se quer entrar com outra conta.",
"Confirm encryption setup": "Confirmar a configuração de criptografia",
"Click the button below to confirm setting up encryption.": "Clique no botão abaixo para confirmar a configuração da criptografia.",
- "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Proteja-se contra a perda de acesso a mensagens e dados criptografados fazendo a cópia segura (backup) das chaves de criptografia no seu servidor.",
+ "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Proteja-se contra a perda de acesso a mensagens e dados criptografados fazendo backup das chaves de criptografia no seu servidor.",
"Generate a Security Key": "Gerar uma Chave de Segurança",
"We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Nós geramos uma Chave de Segurança para você. Por favor, guarde-a em um lugar seguro, como um gerenciador de senhas ou um cofre.",
- "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use uma frase secreta que apenas você conhece, e opcionalmente salve uma Chave de Segurança para usar como cópia de segurança (backup).",
- "Restore your key backup to upgrade your encryption": "Restaurar a sua cópia segura (backup) de chaves para atualizar a sua criptografia",
+ "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use uma frase secreta que apenas você conhece, e opcionalmente salve uma Chave de Segurança para acessar o backup.",
+ "Restore your key backup to upgrade your encryption": "Restaurar o backup das suas chaves para atualizar a sua criptografia",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Atualize esta sessão para permitir que ela confirme outras sessões, dando a elas acesso às mensagens criptografadas e marcando-as como confiáveis para os seus contatos.",
"Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Guarde sua Chave de Segurança em algum lugar seguro, como por exemplo um gestor de senhas ou um cofre, já que esta chave é a proteção para seus dados criptografados.",
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Se você cancelar agora, poderá perder mensagens e dados criptografados se você perder acesso aos seus logins atuais.",
"Upgrade your encryption": "Atualizar sua criptografia",
"Save your Security Key": "Salve sua Chave de Segurança",
- "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Nós vamos armazenar uma cópia criptografada de suas chaves no nosso servidor. Por favor, proteja esta cópia (backup) com uma frase de recuperação.",
+ "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Nós armazenaremos uma cópia criptografada de suas chaves no nosso servidor. Por favor, proteja este backup com uma frase de recuperação.",
"Set up with a recovery key": "Configurar com uma chave de recuperação",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Sua chave de recuperação é uma rede de proteção - você pode usá-la para restaurar o acesso às suas mensagens criptografadas se você esquecer sua frase de recuperação.",
"Your recovery key": "Sua chave de recuperação",
@@ -1601,12 +1601,12 @@
"Your recovery key is in your Downloads folder.": "Sua chave de recuperação está na sua pasta de Downloads.",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Sem configurar a Recuperação Segura de Mensagens, você não será capaz de restaurar seu histórico de mensagens criptografadas e fizer logout ou usar outra sessão.",
"Make a copy of your recovery key": "Fazer uma cópia de sua chave de recuperação",
- "Starting backup...": "Iniciando cópia de segurança (backup)...",
- "Create key backup": "Criar cópia de segurança (backup) da chave",
+ "Starting backup...": "Começando o backup...",
+ "Create key backup": "Criar backup de chave",
"A new recovery passphrase and key for Secure Messages have been detected.": "Uma nova frase e chave de recuperação para Mensagens Seguras foram detectadas.",
- "This session is encrypting history using the new recovery method.": "Esta sessão está criptografando o histórico de mensagens usando o novo método de restauração.",
+ "This session is encrypting history using the new recovery method.": "Esta sessão está criptografando o histórico de mensagens usando a nova opção de recuperação.",
"This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "Esta sessão detectou que sua frase e chave de recuperação para Mensagens Seguras foram removidas.",
- "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Se você fez isso acidentalmente, você pode configurar Mensagens Seguras nesta sessão, o que vai re-criptografar o histórico de mensagens desta sessão com um novo método de recuperação.",
+ "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Se você fez isso acidentalmente, você pode configurar Mensagens Seguras nesta sessão, o que vai re-criptografar o histórico de mensagens desta sessão com uma nova opção de recuperação.",
"If disabled, messages from encrypted rooms won't appear in search results.": "Se desativado, as mensagens de salas criptografadas não aparecerão em resultados de buscas.",
"%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s está armazenando de forma segura as mensagens criptografadas localmente, para que possam aparecer nos resultados das buscas:",
"%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s de %(totalRooms)s",
@@ -1642,7 +1642,7 @@
"Edited at %(date)s. Click to view edits.": "Editado em %(date)s. Clique para ver edições.",
"edited": "editado",
"Can't load this message": "Não foi possível carregar esta mensagem",
- "Submit logs": "Enviar registros",
+ "Submit logs": "Enviar relatórios",
"Frequently Used": "Mais usados",
"Animals & Nature": "Animais e natureza",
"Food & Drink": "Comidas e bebidas",
@@ -1722,7 +1722,7 @@
"Published Addresses": "Endereços publicados",
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Os endereços publicados podem ser usados por qualquer pessoa em qualquer servidor para entrar na sala. Para publicar um endereço, primeiramente ele precisa ser definido como um endereço local.",
"Other published addresses:": "Outros endereços publicados:",
- "New published address (e.g. #alias:server)": "Novo endereço publicado (por exemplo, #apelido:server)",
+ "New published address (e.g. #alias:server)": "Novo endereço publicado (por exemplo, #nome:server)",
"Local Addresses": "Endereços locais",
"%(name)s cancelled verifying": "%(name)s cancelou a confirmação",
"Your display name": "Seu nome e sobrenome",
@@ -1889,12 +1889,12 @@
"All settings": "Todas as configurações",
"You're signed out": "Você está desconectada/o",
"Clear personal data": "Limpar dados pessoais",
- "Command Autocomplete": "Preenchimento automático de comandos",
+ "Command Autocomplete": "Preenchimento automático do comando",
"Community Autocomplete": "Preenchimento automático da comunidade",
"DuckDuckGo Results": "Resultados no DuckDuckGo",
- "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se você não excluiu o método de recuperação, um invasor pode estar tentando acessar sua conta. Altere a senha da sua conta e defina imediatamente um novo método de recuperação nas Configurações.",
+ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se você não excluiu a opção de recuperação, um invasor pode estar tentando acessar sua conta. Altere a senha da sua conta e defina imediatamente uma nova opção de recuperação nas Configurações.",
"Room List": "Lista de salas",
- "Autocomplete": "Autocompletar",
+ "Autocomplete": "Preencher automaticamente",
"Alt": "Alt",
"Alt Gr": "Alt Gr",
"Shift": "Shift",
@@ -2056,11 +2056,11 @@
"You'll upgrade this room from to .": "Você atualizará esta sala de para .",
"A username can only contain lower case letters, numbers and '=_-./'": "Um nome de usuário só pode ter letras minúsculas, números e '=_-./'",
"Command Help": "Ajuda com Comandos",
- "To help us prevent this in future, please send us logs.": "Para nos ajudar a evitar isso no futuro, envie-nos os registros.",
+ "To help us prevent this in future, please send us logs.": "Para nos ajudar a evitar isso no futuro, envie-nos os relatórios.",
"Your browser likely removed this data when running low on disk space.": "O seu navegador provavelmente removeu esses dados quando o espaço de armazenamento ficou insuficiente.",
"Integration Manager": "Gerenciador de Integrações",
"Find others by phone or email": "Encontre outras pessoas por telefone ou e-mail",
- "Use bots, bridges, widgets and sticker packs": "Use bots, pontes, widgets e pacotes de figurinhas",
+ "Use bots, bridges, widgets and sticker packs": "Use bots, integrações, widgets e pacotes de figurinhas",
"Terms of Service": "Termos de serviço",
"To continue you need to accept the terms of this service.": "Para continuar, você precisa aceitar os termos deste serviço.",
"Service": "Serviço",
@@ -2308,8 +2308,8 @@
"Room Info": "Informações da sala",
"Widgets": "Widgets",
"Unpin app": "Desafixar app",
- "Edit widgets, bridges & bots": "Editar widgets, pontes & bots",
- "Add widgets, bridges & bots": "Adicionar widgets, pontes & bots",
+ "Edit widgets, bridges & bots": "Editar widgets, integrações & bots",
+ "Add widgets, bridges & bots": "Adicionar widgets, integrações & bots",
"%(count)s people|other": "%(count)s pessoas",
"%(count)s people|one": "%(count)s pessoa",
"Show files": "Mostrar arquivos",
@@ -2329,5 +2329,145 @@
"What's the name of your community or team?": "Qual é o nome da sua comunidade ou equipe?",
"Add image (optional)": "Adicionar foto (opcional)",
"An image will help people identify your community.": "Uma foto ajudará as pessoas identificarem a sua comunidade.",
- "Preview": "Visualizar"
+ "Preview": "Visualizar",
+ "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Adiciona ( ͡° ͜ʖ ͡°) a uma mensagem de texto",
+ "Set up Secure Backup": "Configurar o backup online",
+ "Safeguard against losing access to encrypted messages & data": "Proteja-se contra a perda de acesso a mensagens & dados criptografados",
+ "Show message previews for reactions in DMs": "Mostrar pré-visualizações para reações em mensagens privadas",
+ "Show message previews for reactions in all rooms": "Mostrar pré-visualizações para reações em todas as salas",
+ "Uploading logs": "Enviando relatórios",
+ "Downloading logs": "Baixando relatórios",
+ "Backup version:": "Versão do backup:",
+ "Backup key stored:": "Backup da chave armazenada:",
+ "Backup key cached:": "Backup da chave em cache:",
+ "Secure Backup": "Backup online",
+ "Your keys are being backed up (the first backup could take a few minutes).": "O backup de suas chaves está sendo feito (o primeiro backup pode demorar alguns minutos).",
+ "Secure your backup with a recovery passphrase": "Proteja o seu backup com uma frase de recuperação",
+ "You can also set up Secure Backup & manage your keys in Settings.": "Você também pode configurar o Backup online & configurar as suas senhas nas Configurações.",
+ "End conference": "Terminar conferência",
+ "This will end the conference for everyone. Continue?": "Isso encerrará a chamada para todos. Prosseguir?",
+ "Cross-signing is ready for use.": "A autoverificação está pronta para uso.",
+ "Cross-signing is not set up.": "A autoverificação não está configurada.",
+ "Reset": "Redefinir",
+ "not found in storage": "não encontrado no armazenamento",
+ "Master private key:": "Chave privada principal:",
+ "Failed to save your profile": "Houve uma falha ao salvar o seu perfil",
+ "The operation could not be completed": "A operação não foi concluída",
+ "Algorithm:": "Algoritmo:",
+ "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Faça backup de suas chaves de criptografia com os dados da sua conta, para se prevenir a perder o acesso às suas sessões. Suas chaves serão protegidas com uma chave de recuperação exclusiva.",
+ "Secret storage:": "Armazenamento secreto:",
+ "ready": "pronto",
+ "not ready": "não está pronto",
+ "Subscribed lists": "Listas inscritas",
+ "This room is bridging messages to the following platforms. Learn more.": "Esta sala está integrando mensagens com as seguintes plataformas. Saiba mais.",
+ "This room isn’t bridging messages to any platforms. Learn more.": "Esta sala não está integrando mensagens com nenhuma plataforma. Saiba mais.",
+ "Bridges": "Integrações",
+ "Error changing power level requirement": "Houve um erro ao alterar o nível de permissão do contato",
+ "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Ocorreu um erro ao alterar os níveis de permissão da sala. Certifique-se de que você tem o nível suficiente e tente novamente.",
+ "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Ocorreu um erro ao alterar o nível de permissão de um contato. Certifique-se de que você tem o nível suficiente e tente novamente.",
+ "Remove messages sent by others": "Remover mensagens enviadas por outros",
+ "To link to this room, please add an address.": "Para criar um link para esta sala, antes adicione um endereço.",
+ "Explore community rooms": "Explorar salas da comunidade",
+ "Explore public rooms": "Explorar salas públicas",
+ "Can't see what you’re looking for?": "Não consegue encontrar o que está procurando?",
+ "Explore all public rooms": "Explorar todas as salas públicas",
+ "%(count)s results|other": "%(count)s resultados",
+ "%(count)s results|one": "%(count)s resultado",
+ "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s apareceu ao tentar entrar na sala. Se você recebeu essa mensagem por engano, envie um relatório de erro.",
+ "Not encrypted": "Não criptografada",
+ "About": "Sobre a sala",
+ "Pin to room": "Fixar na sala",
+ "You can only pin 2 widgets at a time": "Você só pode fixar 2 widgets ao mesmo tempo",
+ "Ignored attempt to disable encryption": "A tentativa de desativar a criptografia foi ignorada",
+ "Message Actions": "Ações da mensagem",
+ "Join the conference at the top of this room": "Entre na chamada em grupo no topo desta sala",
+ "Join the conference from the room information card on the right": "Participe da chamada em grupo, clicando no botão de informações da sala, à direita da tela",
+ "Video conference ended by %(senderName)s": "Chamada de vídeo em grupo encerrada por %(senderName)s",
+ "Video conference updated by %(senderName)s": "Chamada de vídeo em grupo atualizada por %(senderName)s",
+ "Video conference started by %(senderName)s": "Chamada de vídeo em grupo iniciada por %(senderName)s",
+ "Preparing to download logs": "Preparando os relatórios para download",
+ "Download logs": "Baixar relatórios",
+ "Use this when referencing your community to others. The community ID cannot be changed.": "Use esta informação para indicar a sua comunidade para outras pessoas. O ID da comunidade não pode ser alterado.",
+ "You can change this later if needed.": "Você poderá alterar esta informação posteriormente, se for necessário.",
+ "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Salas privadas são encontradas e acessadas apenas por meio de convite. Por sua vez, salas públicas são encontradas e acessadas por qualquer pessoa.",
+ "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Salas privadas são encontradas e acessadas apenas por meio de convite. Por sua vez, salas públicas são encontradas e acessadas por qualquer integrante desta comunidade.",
+ "Your server requires encryption to be enabled in private rooms.": "O seu servidor demanda que a criptografia esteja ativada em salas privadas.",
+ "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Você pode ativar essa opção se a sala for usada apenas para colaboração dentre equipes internas em seu servidor local. Essa opção não poderá ser alterado mais tarde.",
+ "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Você pode desativar essa opção se a sala for usada para colaboração dentre equipes externas que possuem seu próprio servidor local. Isso não poderá ser alterado mais tarde.",
+ "Create a room in %(communityName)s": "Criar uma sala em %(communityName)s",
+ "Block anyone not part of %(serverName)s from ever joining this room.": "Bloquear pessoas externas ao servidor %(serverName)s de conseguirem entrar nesta sala.",
+ "Confirm your account deactivation by using Single Sign On to prove your identity.": "Prove a sua identidade por meio do seu Acesso único, para confirmar a desativação da sua conta.",
+ "There was an error updating your community. The server is unable to process your request.": "Houve um erro ao atualizar a sua comunidade. O servidor não conseguiu processar a sua solicitação.",
+ "Update community": "Atualizar a comunidade",
+ "To continue, use Single Sign On to prove your identity.": "Para continuar, use o Acesso único para provar a sua identidade.",
+ "May include members not in %(communityName)s": "Pode incluir integrantes externos à %(communityName)s",
+ "Start a conversation with someone using their name or username (like ).": "Comece uma conversa, a partir do nome ou nome de usuário de alguém (por exemplo: ).",
+ "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "Ninguém será convidado para %(communityName)s. Para convidar alguém para %(communityName)s, clique aqui",
+ "Go": "Próximo",
+ "Invite someone using their name, username (like ) or share this room.": "Convide alguém a partir do nome ou nome de usuário (por exemplo: ) ou compartilhe esta sala.",
+ "Confirm by comparing the following with the User Settings in your other session:": "Para confirmar, compare a seguinte informação com aquela apresentada em sua outra sessão:",
+ "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Atualizar esta sala irá fechar a instância atual da sala e, em seu lugar, criar uma sala atualizada com o mesmo nome. Para oferecer a melhor experiência possível aos integrantes da sala, nós iremos:",
+ "You're all caught up.": "Tudo em dia.",
+ "Your area is experiencing difficulties connecting to the internet.": "A sua região está com dificuldade de acesso à internet.",
+ "A connection error occurred while trying to contact the server.": "Um erro ocorreu na conexão do Element com o servidor.",
+ "Unable to set up keys": "Não foi possível configurar as chaves",
+ "Unpin": "Desafixar",
+ "Cannot create rooms in this community": "Não foi possível criar salas nesta comunidade",
+ "You do not have permission to create rooms in this community.": "Você não tem permissão para criar salas nesta comunidade.",
+ "You’re all caught up": "Tudo em dia",
+ "Explore rooms in %(communityName)s": "Explore as salas em %(communityName)s",
+ "Create community": "Criar comunidade",
+ "Failed to find the general chat for this community": "Houve uma falha para encontrar a conversa principal desta comunidade",
+ "Community settings": "Configurações da comunidade",
+ "User settings": "Configurações do usuário",
+ "Community and user menu": "Comunidade e menu de usuário",
+ "Sign in instead": "Fazer login",
+ "Failed to get autodiscovery configuration from server": "Houve uma falha para obter do servidor a configuração de encontrar contatos",
+ "If you've joined lots of rooms, this might take a while": "Se você participa em muitas salas, isso pode demorar um pouco",
+ "Unable to query for supported registration methods.": "Não foi possível consultar as opções de registro suportadas.",
+ "Registration has been disabled on this homeserver.": "O registro de contas foi desativado neste servidor local.",
+ "Continue with previous account": "Continuar com a conta anterior",
+ "This requires the latest %(brand)s on your other devices:": "Esta funcionalidade requer o %(brand)s mais recente em seus outros aparelhos:",
+ "Emoji Autocomplete": "Preenchimento automático de emoji",
+ "Room Autocomplete": "Preenchimento automático de sala",
+ "User Autocomplete": "Preenchimento automático de usuário",
+ "Enter a recovery passphrase": "Digite uma frase de recuperação",
+ "Great! This recovery passphrase looks strong enough.": "Ótimo! Essa frase de recuperação é forte o suficiente.",
+ "Please enter your recovery passphrase a second time to confirm.": "Digite a sua frase de recuperação uma segunda vez para confirmar, por favor.",
+ "Repeat your recovery passphrase...": "Digite a sua frase de recuperação novamente...",
+ "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Mantenha uma cópia em algum lugar seguro, como em um gerenciador de senhas ou até mesmo em um cofre.",
+ "Unable to query secret storage status": "Não foi possível obter o status do armazenamento secreto",
+ "Set a Security Phrase": "Defina uma frase de segurança",
+ "Confirm Security Phrase": "Confirme a frase de segurança",
+ "Unable to set up secret storage": "Não foi possível definir o armazenamento secreto",
+ "Recovery Method Removed": "Opção de recuperação removida",
+ "Not currently indexing messages for any room.": "Atualmente, mensagens de nenhuma sala estão sendo armazenadas.",
+ "Currently indexing: %(currentRoom)s": "Armazenando no momento: %(currentRoom)s",
+ "Indexed messages:": "Mensagens armazenadas:",
+ "Indexed rooms:": "Salas armazenadas:",
+ "Message downloading sleep time(ms)": "Tempo de espera entre o download de mensagens (ms)",
+ "Clear room list filter field": "Limpar o campo de busca de salas",
+ "Previous/next unread room or DM": "Anterior/próxima mensagem ou sala não lida",
+ "Previous/next room or DM": "Anterior/próxima mensagem ou sala",
+ "Toggle the top left menu": "Alternar o menu superior esquerdo",
+ "Activate selected button": "Apertar no botão selecionado",
+ "Toggle right panel": "Alternar o painel na direita",
+ "Toggle this dialog": "Alternar esta janela",
+ "Move autocomplete selection up/down": "Alternar para cima/baixo a opção do preenchimento automático",
+ "Cancel autocomplete": "Cancelar o preenchimento automático",
+ "Offline encrypted messaging using dehydrated devices": "Envio de mensagens criptografadas offline, usando dispositivos específicos",
+ "Calling...": "Chamando...",
+ "Call connecting...": "Iniciando chamada...",
+ "Starting camera...": "Iniciando a câmera...",
+ "Starting microphone...": "Iniciando o microfone...",
+ "(their device couldn't start the camera / microphone)": "(o aparelho não conseguiu iniciar a câmera/microfone)",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s alterou a lista de controle de acesso do servidor para esta sala.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s definiu a lista de controle de acesso do servidor para esta sala.",
+ "The call could not be established": "Não foi possível iniciar a chamada",
+ "The other party declined the call.": "O contato recusou a chamada.",
+ "%(senderName)s declined the call.": "%(senderName)s recusou a chamada.",
+ "(an error occurred)": "(ocorreu um erro)",
+ "(connection failed)": "(a conexão falhou)",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Todos os servidores foram banidos desta sala! Esta sala não pode mais ser utilizada.",
+ "Call Declined": "Chamada recusada"
}
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 5dd9c66688..70a96a87bf 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2517,5 +2517,10 @@
"End conference": "Завершить конференцию",
"This will end the conference for everyone. Continue?": "Это завершит конференцию для всех. Продолжить?",
"Failed to save your profile": "Не удалось сохранить ваш профиль",
- "The operation could not be completed": "Операция не может быть выполнена"
+ "The operation could not be completed": "Операция не может быть выполнена",
+ "Calling...": "Звонок…",
+ "Call connecting...": "Устанавливается соединение…",
+ "Starting camera...": "Запуск камеры…",
+ "Starting microphone...": "Запуск микрофона…",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Все серверы запрещены к участию! Эта комната больше не может быть использована."
}
diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 6b61316356..84a8ac2786 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2518,5 +2518,21 @@
"This version of %(brand)s does not support viewing some encrypted files": "Ky version i %(brand)s nuk mbulon parjen për disa kartela të fshehtëzuara",
"This version of %(brand)s does not support searching encrypted messages": "Ky version i %(brand)s nuk mbulon kërkimin në mesazhe të fshehtëzuar",
"Cannot create rooms in this community": "S’mund të krijohen dhoma në këtë bashkësi",
- "You do not have permission to create rooms in this community.": "S’keni leje të krijoni dhoma në këtë bashkësi."
+ "You do not have permission to create rooms in this community.": "S’keni leje të krijoni dhoma në këtë bashkësi.",
+ "Failed to save your profile": "S’u arrit të ruhej profili juaj",
+ "The operation could not be completed": "Veprimi s’u plotësua dot",
+ "Starting microphone...": "Po vihet mikrofoni në punë…",
+ "Starting camera...": "Po vihet kamera në punë…",
+ "Call connecting...": "Po bëhet lidhja për thirrje…",
+ "Calling...": "Po thirret…",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Janë dëbuar nga pjesëmarrja krejt shërbyesit! Kjo dhomë s’mund të përdoret më.",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s ndryshoi ACL-ra shërbyesi për këtë dhomë.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s caktoi ACL-ra shërbyesi për këtë dhomë.",
+ "%(senderName)s declined the call.": "%(senderName)s hodhi poshtë thirrjen.",
+ "(an error occurred)": "(ndodhi një gabim)",
+ "(their device couldn't start the camera / microphone)": "(pajisja e tyre s’vuri dot në punë kamerën / mikrofonin)",
+ "(connection failed)": "(dështoi lidhja)",
+ "The call could not be established": "Thirrja s’u nis dot",
+ "The other party declined the call.": "Pala tjetër hodhi poshtë thirrjen.",
+ "Call Declined": "Thirrja u Hodh Poshtë"
}
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 6283e8b945..4a74d88918 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2457,5 +2457,19 @@
"This version of %(brand)s does not support viewing some encrypted files": "Den här versionen av %(brand)s stöder inte visning av vissa krypterade filer",
"This version of %(brand)s does not support searching encrypted messages": "Den här versionen av %(brand)s stöder inte sökning bland krypterade meddelanden",
"Cannot create rooms in this community": "Kan inte skapa rum i den här gemenskapen",
- "You do not have permission to create rooms in this community.": "Du har inte behörighet att skapa rum i den här gemenskapen."
+ "You do not have permission to create rooms in this community.": "Du har inte behörighet att skapa rum i den här gemenskapen.",
+ "Calling...": "Ringer…",
+ "Call connecting...": "Samtal ansluts…",
+ "Starting camera...": "Startar kamera…",
+ "Starting microphone...": "Startar mikrofon…",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Alla servrar har bannats från att delta! Det här rummet kan inte längre användas.",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s ändrade server-ACL:erna för det här rummet.",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s ställde in server-ACL:er för det här rummet.",
+ "%(senderName)s declined the call.": "%(senderName)s avböjde samtalet.",
+ "(an error occurred)": "(ett fel inträffade)",
+ "(their device couldn't start the camera / microphone)": "(deras enhet kunde inte starta kameran/mikrofonen)",
+ "(connection failed)": "(anslutning misslyckad)",
+ "The call could not be established": "Samtalet kunde inte etableras",
+ "The other party declined the call.": "Den andra parten avböjde samtalet.",
+ "Call Declined": "Samtal avböjt"
}
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 4499d6d124..4151a3f755 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -222,7 +222,7 @@
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Чи використовуєте ви режим форматованого тексту у редакторі Rich Text Editor",
"Your homeserver's URL": "URL адреса вашого домашнього сервера",
"Failed to verify email address: make sure you clicked the link in the email": "Не вдалось перевірити адресу електронної пошти: переконайтесь, що ви перейшли за посиланням у листі",
- "The platform you're on": "Використовувана платформа",
+ "The platform you're on": "Платформа, на якій ви працюєте",
"e.g. %(exampleValue)s": "напр. %(exampleValue)s",
"Every page you use in the app": "Кожна сторінка, яку ви використовуєте в програмі",
"e.g. ": "напр. ",
@@ -1229,5 +1229,40 @@
"Cancel autocomplete": "Скасувати самодоповнення",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Журнали зневадження містять дані використання застосунку, включно з вашим користувацьким ім’ям, ідентифікаторами або псевдонімами відвіданих вами кімнат або груп, а також іменами інших користувачів. Вони не містять повідомлень.",
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Підтвердьте знедіяння вашого облікового запису через Single Sign On щоб підтвердити вашу особу.",
- "This account has been deactivated.": "Цей обліковий запис було знедіяно."
+ "This account has been deactivated.": "Цей обліковий запис було знедіяно.",
+ "End conference": "Завершити конференцію",
+ "This will end the conference for everyone. Continue?": "Це завершить конференцію для всіх. Продовжити?",
+ "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Додає ( ͡° ͜ʖ ͡°) на початку текстового повідомлення",
+ "about a day ago": "близько доби тому",
+ "%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
+ "Unexpected server error trying to leave the room": "Виникла неочікувана помилка серверу під час спроби залишити кімнату",
+ "Unknown App": "Невідомий додаток",
+ "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Відправляти анонімну статистику користування, що дозволяє нам покращувати %(brand)s. Це використовує кукі.",
+ "Set up Secure Backup": "Налаштувати захищене резервне копіювання",
+ "Safeguard against losing access to encrypted messages & data": "Захист від втрати доступу до зашифрованих повідомлень та даних",
+ "The person who invited you already left the room.": "Особа, що вас запросила, вже залишила кімнату.",
+ "The person who invited you already left the room, or their server is offline.": "Особа, що вас запросила вже залишила кімнату, або її сервер відімкнено.",
+ "Change notification settings": "Змінити налаштування сповіщень",
+ "Render simple counters in room header": "Показувати звичайні лічильники у заголовку кімнати",
+ "Send typing notifications": "Надсилати сповіщення про набирання тексту",
+ "Use a system font": "Використовувати системний шрифт",
+ "System font name": "Ім’я системного шрифту",
+ "Allow Peer-to-Peer for 1:1 calls": "Дозволити Peer-to-Peer для дзвінків 1:1",
+ "Enable widget screenshots on supported widgets": "Увімкнути скріншоти віджетів для віджетів, що підтримуються",
+ "Prompt before sending invites to potentially invalid matrix IDs": "Запитувати перед надсиланням запрошень на потенційно недійсні matrix ID",
+ "Order rooms by name": "Сортувати кімнати за назвою",
+ "Low bandwidth mode": "Режим для низької пропускної здатності",
+ "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Дозволити резервний сервер допоміжних викликів turn.matrix.org якщо ваш домашній сервер не пропонує такого (ваша IP-адреса буде розкрита для здійснення дзвінка)",
+ "Send read receipts for messages (requires compatible homeserver to disable)": "Надсилати мітки прочитання повідомлень (необхідний сумісний домашній сервер для відімкнення)",
+ "How fast should messages be downloaded.": "Як швидко повідомлення повинні завантажуватися.",
+ "Enable experimental, compact IRC style layout": "Увімкнути експериментальне, компактне компонування IRC",
+ "Uploading logs": "Відвантаження журналів",
+ "Downloading logs": "Завантаження журналів",
+ "My Ban List": "Мій список блокувань",
+ "This is your list of users/servers you have blocked - don't leave the room!": "Це ваш список користувачів/серверів, які ви заблокували – не залишайте кімнату!",
+ "Incoming call": "Вхідний виклик",
+ "The other party cancelled the verification.": "Друга сторона скасувала звірення.",
+ "Verified!": "Звірено!",
+ "You've successfully verified this user.": "Ви успішно звірили цього користувача.",
+ "Got It": "Зрозуміло"
}
diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index eeb119dfd4..359dcfc4a0 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -1529,10 +1529,10 @@
"Backup has a signature from unknown session with ID %(deviceId)s": "备份有来自 ID 为 %(deviceId)s 的未知会话的签名",
"Backup has a valid signature from this session": "备份有来自此会话的有效签名",
"Backup has an invalid signature from this session": "备份有来自此会话的无效签名",
- "Backup has a valid signature from verified session ": "备份有来自已验证的会话 的有效签名",
- "Backup has a valid signature from unverified session ": "备份有来自未验证会话 的无效签名",
- "Backup has an invalid signature from verified session ": "备份有来自已验证会话 的无效签名",
- "Backup has an invalid signature from unverified session ": "备份有来自未验证的会话 的 无效签名",
+ "Backup has a valid signature from verified session ": "备份有一个有效的签名,它来自已验证的会话",
+ "Backup has a valid signature from unverified session ": "备份有一个有效的签名,它来自未验证的会话\n",
+ "Backup has an invalid signature from verified session ": "备份有一个无效的签名,它来自已验证的会话\n",
+ "Backup has an invalid signature from unverified session ": "备份有一个无效的签名,它来自未验证的会话\n",
"Backup is not signed by any of your sessions": "备份没有被您的任何一个会话签名",
"This backup is trusted because it has been restored on this session": "此备份是受信任的因为它被恢复到了此会话上",
"Backup key stored: ": "存储的备份密钥: ",
@@ -1553,12 +1553,12 @@
"Disconnect identity server": "断开身份服务器连接",
"Disconnect from the identity server ?": "从身份服务器 断开连接吗?",
"Disconnect": "断开连接",
- "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "在断开连接之前,您应该从身份服务器 删除您的个人信息。不幸的是,身份服务器 现在为离线状态或不能到达。",
+ "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "断开连接前,你应当删除你的个人信息从身份服务器。\n不幸的是,身份服务器当前处于离线状态或无法访问。",
"You should:": "您应该:",
"contact the administrators of identity server ": "联系身份服务器 的管理员",
"wait and try again later": "等待并稍后重试",
"Disconnect anyway": "仍然断开连接",
- "You are still sharing your personal data on the identity server .": "您仍然在身份服务器 上共享您的个人信息。",
+ "You are still sharing your personal data on the identity server .": "您仍然在分享您的个人信息在身份服务器上。",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "我们推荐您在断开连接前从身份服务器上删除您的邮箱地址和电话号码。",
"Identity Server (%(server)s)": "身份服务器(%(server)s)",
"not stored": "未存储",
@@ -2287,7 +2287,7 @@
"Alt": "Alt",
"Alt Gr": "Alt Gr",
"Shift": "Shift",
- "Super": "Super",
+ "Super": "",
"Ctrl": "Ctrl",
"New line": "换行",
"Jump to start/end of the composer": "跳转到编辑器的开始/结束",
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index da93d6c270..0ae9b7edb0 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2527,5 +2527,19 @@
"Offline encrypted messaging using dehydrated devices": "使用乾淨裝置的離線加密訊息",
"Failed to save your profile": "儲存您的設定檔失敗",
"The operation could not be completed": "無法完成操作",
- "Remove messages sent by others": "移除其他人傳送的訊息"
+ "Remove messages sent by others": "移除其他人傳送的訊息",
+ "Calling...": "正在通話……",
+ "Call connecting...": "正在連線通話……",
+ "Starting camera...": "正在開啟攝影機……",
+ "Starting microphone...": "正在開啟麥克風……",
+ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 所有伺服器都被禁止加入! 這間聊天室無法使用。",
+ "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s 為此房間更改了伺服器的存取控制列表。",
+ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s 為此房間設置了伺服器的存取控制列表。",
+ "%(senderName)s declined the call.": "%(senderName)s 拒絕了通話。",
+ "(an error occurred)": "(遇到錯誤)",
+ "(their device couldn't start the camera / microphone)": "(他們的裝置無法開啟攝影機/麥克風)",
+ "(connection failed)": "(連線失敗)",
+ "The call could not be established": "無法建立通話",
+ "The other party declined the call.": "對方拒絕了電話。",
+ "Call Declined": "通話已拒絕"
}
diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts
index 64cf01bd6e..2474406618 100644
--- a/src/indexing/BaseEventIndexManager.ts
+++ b/src/indexing/BaseEventIndexManager.ts
@@ -105,10 +105,13 @@ export default abstract class BaseEventIndexManager {
/**
* Initialize the event index for the given user.
*
+ * @param {string} userId The event that should be added to the index.
+ * @param {string} deviceId The profile of the event sender at the
+ *
* @return {Promise} A promise that will resolve when the event index is
* initialized.
*/
- async initEventIndex(): Promise {
+ async initEventIndex(userId: string, deviceId: string): Promise {
throw new Error("Unimplemented");
}
diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js
index 58e8430825..443daa8f43 100644
--- a/src/indexing/EventIndexPeg.js
+++ b/src/indexing/EventIndexPeg.js
@@ -21,6 +21,7 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg";
import EventIndex from "../indexing/EventIndex";
+import {MatrixClientPeg} from "../MatrixClientPeg";
import SettingsStore from '../settings/SettingsStore';
import {SettingLevel} from "../settings/SettingLevel";
@@ -70,9 +71,13 @@ class EventIndexPeg {
async initEventIndex() {
const index = new EventIndex();
const indexManager = PlatformPeg.get().getEventIndexingManager();
+ const client = MatrixClientPeg.get();
+
+ const userId = client.getUserId();
+ const deviceId = client.getDeviceId();
try {
- await indexManager.initEventIndex();
+ await indexManager.initEventIndex(userId, deviceId);
const userVersion = await indexManager.getUserVersion();
const eventIndexIsEmpty = await indexManager.isEventIndexEmpty();
@@ -83,7 +88,7 @@ class EventIndexPeg {
await indexManager.closeEventIndex();
await this.deleteEventIndex();
- await indexManager.initEventIndex();
+ await indexManager.initEventIndex(userId, deviceId);
await indexManager.setUserVersion(INDEX_VERSION);
}
diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts
index 1791e92514..a29c74c5eb 100644
--- a/src/integrations/IntegrationManagers.ts
+++ b/src/integrations/IntegrationManagers.ts
@@ -120,7 +120,7 @@ export class IntegrationManagers {
if (!data) return;
const uiUrl = w.content['url'];
- const apiUrl = data['api_url'];
+ const apiUrl = data['api_url'] as string;
if (!apiUrl || !uiUrl) return;
const manager = new IntegrationManagerInstance(
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index e699f8e301..0921b65137 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -96,7 +96,7 @@ function safeCounterpartTranslate(text: string, options?: object) {
return translated;
}
-interface IVariables {
+export interface IVariables {
count?: number;
[key: string]: number | string;
}
diff --git a/src/resizer/distributors/collapse.js b/src/resizer/distributors/collapse.ts
similarity index 64%
rename from src/resizer/distributors/collapse.js
rename to src/resizer/distributors/collapse.ts
index 784532a0eb..ddf3bd687e 100644
--- a/src/resizer/distributors/collapse.js
+++ b/src/resizer/distributors/collapse.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,9 +16,16 @@ limitations under the License.
import FixedDistributor from "./fixed";
import ResizeItem from "../item";
+import Resizer, {IConfig} from "../resizer";
+import Sizer from "../sizer";
-class CollapseItem extends ResizeItem {
- notifyCollapsed(collapsed) {
+export interface ICollapseConfig extends IConfig {
+ toggleSize: number;
+ onCollapsed?(collapsed: boolean, id: string, element: HTMLElement): void;
+}
+
+class CollapseItem extends ResizeItem {
+ notifyCollapsed(collapsed: boolean) {
const callback = this.resizer.config.onCollapsed;
if (callback) {
callback(collapsed, this.id, this.domNode);
@@ -26,18 +33,20 @@ class CollapseItem extends ResizeItem {
}
}
-export default class CollapseDistributor extends FixedDistributor {
- static createItem(resizeHandle, resizer, sizer) {
+export default class CollapseDistributor extends FixedDistributor {
+ static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer) {
return new CollapseItem(resizeHandle, resizer, sizer);
}
- constructor(item, config) {
+ private readonly toggleSize: number;
+ private isCollapsed = false;
+
+ constructor(item: CollapseItem) {
super(item);
- this.toggleSize = config && config.toggleSize;
- this.isCollapsed = false;
+ this.toggleSize = item.resizer?.config?.toggleSize;
}
- resize(newSize) {
+ public resize(newSize: number) {
const isCollapsedSize = newSize < this.toggleSize;
if (isCollapsedSize && !this.isCollapsed) {
this.isCollapsed = true;
diff --git a/src/resizer/distributors/fixed.js b/src/resizer/distributors/fixed.ts
similarity index 60%
rename from src/resizer/distributors/fixed.js
rename to src/resizer/distributors/fixed.ts
index e93c6fbcee..64f1c5015b 100644
--- a/src/resizer/distributors/fixed.js
+++ b/src/resizer/distributors/fixed.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@ limitations under the License.
import ResizeItem from "../item";
import Sizer from "../sizer";
+import Resizer, {IConfig} from "../resizer";
/**
distributors translate a moving cursor into
@@ -27,29 +28,42 @@ they have two methods:
within the container bounding box. For internal use.
This method usually ends up calling `resize` once the start offset is subtracted.
*/
-export default class FixedDistributor {
- static createItem(resizeHandle, resizer, sizer) {
+export default class FixedDistributor = ResizeItem> {
+ static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem {
return new ResizeItem(resizeHandle, resizer, sizer);
}
- static createSizer(containerElement, vertical, reverse) {
+ static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer {
return new Sizer(containerElement, vertical, reverse);
}
- constructor(item) {
- this.item = item;
+ private readonly beforeOffset: number;
+
+ constructor(public readonly item: I) {
this.beforeOffset = item.offset();
}
- resize(size) {
+ public get size() {
+ return this.item.getSize();
+ }
+
+ public set size(size: string) {
+ this.item.setRawSize(size);
+ }
+
+ public resize(size: number) {
this.item.setSize(size);
}
- resizeFromContainerOffset(offset) {
+ public resizeFromContainerOffset(offset: number) {
this.resize(offset - this.beforeOffset);
}
- start() {}
+ public start() {
+ this.item.start();
+ }
- finish() {}
+ public finish() {
+ this.item.finish();
+ }
}
diff --git a/src/resizer/distributors/percentage.ts b/src/resizer/distributors/percentage.ts
new file mode 100644
index 0000000000..e9b1425414
--- /dev/null
+++ b/src/resizer/distributors/percentage.ts
@@ -0,0 +1,49 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import Sizer from "../sizer";
+import FixedDistributor from "./fixed";
+import {IConfig} from "../resizer";
+
+class PercentageSizer extends Sizer {
+ public start(item: HTMLElement) {
+ if (this.vertical) {
+ item.style.minHeight = null;
+ } else {
+ item.style.minWidth = null;
+ }
+ }
+
+ public finish(item: HTMLElement) {
+ const parent = item.offsetParent as HTMLElement;
+ if (!parent) return;
+ if (this.vertical) {
+ const p = ((item.offsetHeight / parent.offsetHeight) * 100).toFixed(2) + "%";
+ item.style.minHeight = p;
+ item.style.height = p;
+ } else {
+ const p = ((item.offsetWidth / parent.offsetWidth) * 100).toFixed(2) + "%";
+ item.style.minWidth = p;
+ item.style.width = p;
+ }
+ }
+}
+
+export default class PercentageDistributor extends FixedDistributor {
+ static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean) {
+ return new PercentageSizer(containerElement, vertical, reverse);
+ }
+}
diff --git a/src/resizer/index.js b/src/resizer/index.ts
similarity index 69%
rename from src/resizer/index.js
rename to src/resizer/index.ts
index 1fd8f4da46..9199fc2657 100644
--- a/src/resizer/index.js
+++ b/src/resizer/index.ts
@@ -1,5 +1,4 @@
/*
-Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-export FixedDistributor from "./distributors/fixed";
-export CollapseDistributor from "./distributors/collapse";
-export Resizer from "./resizer";
+export {default as FixedDistributor} from "./distributors/fixed";
+export {default as PercentageDistributor} from "./distributors/percentage";
+export {default as CollapseDistributor} from "./distributors/collapse";
+export {default as Resizer} from "./resizer";
diff --git a/src/resizer/item.js b/src/resizer/item.js
deleted file mode 100644
index 2e06ad217c..0000000000
--- a/src/resizer/item.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
-Copyright 2019 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.
-*/
-
-export default class ResizeItem {
- constructor(handle, resizer, sizer) {
- const id = handle.getAttribute("data-id");
- const reverse = resizer.isReverseResizeHandle(handle);
- const domNode = reverse ? handle.nextElementSibling : handle.previousElementSibling;
-
- this.domNode = domNode;
- this.id = id;
- this.reverse = reverse;
- this.resizer = resizer;
- this.sizer = sizer;
- }
-
- _copyWith(handle, resizer, sizer) {
- const Ctor = this.constructor;
- return new Ctor(handle, resizer, sizer);
- }
-
- _advance(forwards) {
- // opposite direction from fromResizeHandle to get back to handle
- let handle = this.reverse ?
- this.domNode.previousElementSibling :
- this.domNode.nextElementSibling;
- const moveNext = forwards !== this.reverse; // xor
- // iterate at least once to avoid infinite loop
- do {
- if (moveNext) {
- handle = handle.nextElementSibling;
- } else {
- handle = handle.previousElementSibling;
- }
- } while (handle && !this.resizer.isResizeHandle(handle));
-
- if (handle) {
- const nextHandle = this._copyWith(handle, this.resizer, this.sizer);
- nextHandle.reverse = this.reverse;
- return nextHandle;
- }
- }
-
- next() {
- return this._advance(true);
- }
-
- previous() {
- return this._advance(false);
- }
-
- size() {
- return this.sizer.getItemSize(this.domNode);
- }
-
- offset() {
- return this.sizer.getItemOffset(this.domNode);
- }
-
- setSize(size) {
- this.sizer.setItemSize(this.domNode, size);
- const callback = this.resizer.config.onResized;
- if (callback) {
- callback(size, this.id, this.domNode);
- }
- }
-
- clearSize() {
- this.sizer.clearItemSize(this.domNode);
- const callback = this.resizer.config.onResized;
- if (callback) {
- callback(null, this.id, this.domNode);
- }
- }
-
-
- first() {
- const firstHandle = Array.from(this.domNode.parentElement.children).find(el => {
- return this.resizer.isResizeHandle(el);
- });
- if (firstHandle) {
- return this._copyWith(firstHandle, this.resizer, this.sizer);
- }
- }
-
- last() {
- const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => {
- return this.resizer.isResizeHandle(el);
- });
- if (lastHandle) {
- return this._copyWith(lastHandle, this.resizer, this.sizer);
- }
- }
-}
diff --git a/src/resizer/item.ts b/src/resizer/item.ts
new file mode 100644
index 0000000000..3be290f15e
--- /dev/null
+++ b/src/resizer/item.ts
@@ -0,0 +1,125 @@
+/*
+Copyright 2019 - 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import Resizer, {IConfig} from "./resizer";
+import Sizer from "./sizer";
+
+export default class ResizeItem {
+ public readonly domNode: HTMLElement;
+ protected readonly id: string;
+ protected reverse: boolean;
+
+ constructor(
+ handle: HTMLElement,
+ public readonly resizer: Resizer,
+ public readonly sizer: Sizer,
+ ) {
+ this.reverse = resizer.isReverseResizeHandle(handle);
+ this.domNode = (this.reverse ? handle.nextElementSibling : handle.previousElementSibling);
+ this.id = handle.getAttribute("data-id");
+ }
+
+ private copyWith(handle: HTMLElement, resizer: Resizer, sizer: Sizer) {
+ const Ctor = this.constructor as typeof ResizeItem;
+ return new Ctor(handle, resizer, sizer);
+ }
+
+ private advance(forwards: boolean) {
+ // opposite direction from fromResizeHandle to get back to handle
+ let handle = this.reverse ? this.domNode.previousElementSibling : this.domNode.nextElementSibling;
+ const moveNext = forwards !== this.reverse; // xor
+ // iterate at least once to avoid infinite loop
+ do {
+ if (moveNext) {
+ handle = handle.nextElementSibling;
+ } else {
+ handle = handle.previousElementSibling;
+ }
+ } while (handle && !this.resizer.isResizeHandle(handle));
+
+ if (handle) {
+ const nextHandle = this.copyWith(handle, this.resizer, this.sizer);
+ nextHandle.reverse = this.reverse;
+ return nextHandle;
+ }
+ }
+
+ public next() {
+ return this.advance(true);
+ }
+
+ public previous() {
+ return this.advance(false);
+ }
+
+ public size() {
+ return this.sizer.getItemSize(this.domNode);
+ }
+
+ public offset() {
+ return this.sizer.getItemOffset(this.domNode);
+ }
+
+ public start() {
+ this.sizer.start(this.domNode);
+ }
+
+ public finish() {
+ this.sizer.finish(this.domNode);
+ }
+
+ public getSize() {
+ return this.sizer.getDesiredItemSize(this.domNode);
+ }
+
+ public setRawSize(size: string) {
+ this.sizer.setItemSize(this.domNode, size);
+ }
+
+ public setSize(size: number) {
+ this.setRawSize(`${Math.round(size)}px`);
+ const callback = this.resizer.config.onResized;
+ if (callback) {
+ callback(size, this.id, this.domNode);
+ }
+ }
+
+ public clearSize() {
+ this.sizer.clearItemSize(this.domNode);
+ const callback = this.resizer.config.onResized;
+ if (callback) {
+ callback(null, this.id, this.domNode);
+ }
+ }
+
+ public first() {
+ const firstHandle = Array.from(this.domNode.parentElement.children).find(el => {
+ return this.resizer.isResizeHandle(el);
+ });
+ if (firstHandle) {
+ return this.copyWith(firstHandle, this.resizer, this.sizer);
+ }
+ }
+
+ public last() {
+ const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => {
+ return this.resizer.isResizeHandle(el);
+ });
+ if (lastHandle) {
+ return this.copyWith(lastHandle, this.resizer, this.sizer);
+ }
+ }
+}
diff --git a/src/resizer/resizer.js b/src/resizer/resizer.ts
similarity index 55%
rename from src/resizer/resizer.js
rename to src/resizer/resizer.ts
index 1e75bf3bdf..c7c7edcd11 100644
--- a/src/resizer/resizer.js
+++ b/src/resizer/resizer.ts
@@ -1,6 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2018 - 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,86 +14,105 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-/*
-classNames:
+import {throttle} from "lodash";
+
+import FixedDistributor from "./distributors/fixed";
+import ResizeItem from "./item";
+import Sizer from "./sizer";
+
+interface IClassNames {
// class on resize-handle
- handle: string
+ handle?: string;
// class on resize-handle
- reverse: string
+ reverse?: string;
// class on resize-handle
- vertical: string
+ vertical?: string;
// class on container
- resizing: string
-*/
+ resizing?: string;
+}
+export interface IConfig {
+ onResizeStart?(): void;
+ onResizeStop?(): void;
+ onResized?(size: number, id: string, element: HTMLElement): void;
+}
+
+export default class Resizer {
+ private classNames: IClassNames;
-export default class Resizer {
// TODO move vertical/horizontal to config option/container class
// as it doesn't make sense to mix them within one container/Resizer
- constructor(container, distributorCtor, config) {
+ constructor(
+ public container: HTMLElement,
+ private readonly distributorCtor: {
+ new(item: ResizeItem): FixedDistributor;
+ createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem;
+ createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer;
+ },
+ public readonly config?: C,
+ ) {
if (!container) {
throw new Error("Resizer requires a non-null `container` arg");
}
- this.container = container;
- this.distributorCtor = distributorCtor;
- this.config = config;
+
this.classNames = {
handle: "resizer-handle",
reverse: "resizer-reverse",
vertical: "resizer-vertical",
resizing: "resizer-resizing",
};
- this._onMouseDown = this._onMouseDown.bind(this);
}
- setClassNames(classNames) {
+ public setClassNames(classNames: IClassNames) {
this.classNames = classNames;
}
- attach() {
- this.container.addEventListener("mousedown", this._onMouseDown, false);
+ public attach() {
+ this.container.addEventListener("mousedown", this.onMouseDown, false);
+ window.addEventListener("resize", this.onResize);
}
- detach() {
- this.container.removeEventListener("mousedown", this._onMouseDown, false);
+ public detach() {
+ this.container.removeEventListener("mousedown", this.onMouseDown, false);
+ window.removeEventListener("resize", this.onResize);
}
/**
Gives the distributor for a specific resize handle, as if you would have started
to drag that handle. Can be used to manipulate the size of an item programmatically.
@param {number} handleIndex the index of the resize handle in the container
- @return {Distributor} a new distributor for the given handle
+ @return {FixedDistributor} a new distributor for the given handle
*/
- forHandleAt(handleIndex) {
- const handles = this._getResizeHandles();
+ public forHandleAt(handleIndex: number): FixedDistributor {
+ const handles = this.getResizeHandles();
const handle = handles[handleIndex];
if (handle) {
- const {distributor} = this._createSizerAndDistributor(handle);
+ const {distributor} = this.createSizerAndDistributor(handle);
return distributor;
}
}
- forHandleWithId(id) {
- const handles = this._getResizeHandles();
+ public forHandleWithId(id: string): FixedDistributor {
+ const handles = this.getResizeHandles();
const handle = handles.find((h) => h.getAttribute("data-id") === id);
if (handle) {
- const {distributor} = this._createSizerAndDistributor(handle);
+ const {distributor} = this.createSizerAndDistributor(handle);
return distributor;
}
}
- isReverseResizeHandle(el) {
+ public isReverseResizeHandle(el: HTMLElement): boolean {
return el && el.classList.contains(this.classNames.reverse);
}
- isResizeHandle(el) {
+ public isResizeHandle(el: HTMLElement): boolean {
return el && el.classList.contains(this.classNames.handle);
}
- _onMouseDown(event) {
+ private onMouseDown = (event: MouseEvent) => {
// use closest in case the resize handle contains
// child dom nodes that can be the target
- const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`);
+ const resizeHandle = event.target && (event.target).closest(`.${this.classNames.handle}`);
if (!resizeHandle || resizeHandle.parentElement !== this.container) {
return;
}
@@ -109,7 +127,7 @@ export default class Resizer {
this.config.onResizeStart();
}
- const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
+ const {sizer, distributor} = this.createSizerAndDistributor(resizeHandle);
distributor.start();
const onMouseMove = (event) => {
@@ -122,10 +140,10 @@ export default class Resizer {
if (this.classNames.resizing) {
this.container.classList.remove(this.classNames.resizing);
}
+ distributor.finish();
if (this.config.onResizeStop) {
this.config.onResizeStop();
}
- distributor.finish();
body.removeEventListener("mouseup", finishResize, false);
document.removeEventListener("mouseleave", finishResize, false);
body.removeEventListener("mousemove", onMouseMove, false);
@@ -133,21 +151,39 @@ export default class Resizer {
body.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false);
body.addEventListener("mousemove", onMouseMove, false);
- }
+ };
- _createSizerAndDistributor(resizeHandle) {
+ private onResize = throttle(() => {
+ const distributors = this.getDistributors();
+
+ // relax all items if they had any overconstrained flexboxes
+ distributors.forEach(d => d.start());
+ distributors.forEach(d => d.finish());
+ }, 100, {trailing: true, leading: true});
+
+ public getDistributors = () => {
+ return this.getResizeHandles().map(handle => {
+ const {distributor} = this.createSizerAndDistributor(handle);
+ return distributor;
+ });
+ };
+
+ private createSizerAndDistributor(
+ resizeHandle: HTMLDivElement,
+ ): {sizer: Sizer, distributor: FixedDistributor} {
const vertical = resizeHandle.classList.contains(this.classNames.vertical);
const reverse = this.isReverseResizeHandle(resizeHandle);
const Distributor = this.distributorCtor;
const sizer = Distributor.createSizer(this.container, vertical, reverse);
const item = Distributor.createItem(resizeHandle, this, sizer);
- const distributor = new Distributor(item, this.config);
+ const distributor = new Distributor(item);
return {sizer, distributor};
}
- _getResizeHandles() {
+ private getResizeHandles() {
+ if (!this.container.children) return [];
return Array.from(this.container.children).filter(el => {
- return this.isResizeHandle(el);
- });
+ return this.isResizeHandle(el);
+ }) as HTMLElement[];
}
}
diff --git a/src/resizer/sizer.js b/src/resizer/sizer.ts
similarity index 68%
rename from src/resizer/sizer.js
rename to src/resizer/sizer.ts
index 4ce9232457..4de8bb9221 100644
--- a/src/resizer/sizer.js
+++ b/src/resizer/sizer.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
+Copyright 2018 - 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,18 +19,18 @@ implements DOM/CSS operations for resizing.
The sizer determines what CSS mechanism is used for sizing items, like flexbox, ...
*/
export default class Sizer {
- constructor(container, vertical, reverse) {
- this.container = container;
- this.reverse = reverse;
- this.vertical = vertical;
- }
+ constructor(
+ protected readonly container: HTMLElement,
+ protected readonly vertical: boolean,
+ protected readonly reverse: boolean,
+ ) {}
/**
@param {Element} item the dom element being resized
@return {number} how far the edge of the item is from the edge of the container
*/
- getItemOffset(item) {
- const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this._getOffset();
+ public getItemOffset(item: HTMLElement): number {
+ const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this.getOffset();
if (this.reverse) {
return this.getTotalSize() - (offset + this.getItemSize(item));
} else {
@@ -42,41 +42,49 @@ export default class Sizer {
@param {Element} item the dom element being resized
@return {number} the width/height of an item in the container
*/
- getItemSize(item) {
+ public getItemSize(item: HTMLElement): number {
return this.vertical ? item.offsetHeight : item.offsetWidth;
}
/** @return {number} the width/height of the container */
- getTotalSize() {
+ public getTotalSize(): number {
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
}
/** @return {number} container offset to offsetParent */
- _getOffset() {
+ private getOffset(): number {
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
}
/** @return {number} container offset to document */
- _getPageOffset() {
+ private getPageOffset(): number {
let element = this.container;
let offset = 0;
while (element) {
const pos = this.vertical ? element.offsetTop : element.offsetLeft;
offset = offset + pos;
- element = element.offsetParent;
+ element = element.offsetParent;
}
return offset;
}
- setItemSize(item, size) {
+ public getDesiredItemSize(item: HTMLElement) {
if (this.vertical) {
- item.style.height = `${Math.round(size)}px`;
+ return item.style.height;
} else {
- item.style.width = `${Math.round(size)}px`;
+ return item.style.width;
}
}
- clearItemSize(item) {
+ public setItemSize(item: HTMLElement, size: string) {
+ if (this.vertical) {
+ item.style.height = size;
+ } else {
+ item.style.width = size;
+ }
+ }
+
+ public clearItemSize(item: HTMLElement) {
if (this.vertical) {
item.style.height = null;
} else {
@@ -84,17 +92,21 @@ export default class Sizer {
}
}
+ public start(item: HTMLElement) {}
+
+ public finish(item: HTMLElement) {}
+
/**
@param {MouseEvent} event the mouse event
@return {number} the distance between the cursor and the edge of the container,
along the applicable axis (vertical or horizontal)
*/
- offsetFromEvent(event) {
+ public offsetFromEvent(event: MouseEvent) {
const pos = this.vertical ? event.pageY : event.pageX;
if (this.reverse) {
- return (this._getPageOffset() + this.getTotalSize()) - pos;
+ return (this.getPageOffset() + this.getTotalSize()) - pos;
} else {
- return pos - this._getPageOffset();
+ return pos - this.getPageOffset();
}
}
}
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index dbe84c744f..7bdaa3c635 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -632,6 +632,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
default: {},
},
+ "Widgets.leftPanel": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: null,
+ },
[UIFeature.AdvancedEncryption]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.ts
similarity index 86%
rename from src/shouldHideEvent.js
rename to src/shouldHideEvent.ts
index b1533ae835..2a47b9c417 100644
--- a/src/shouldHideEvent.js
+++ b/src/shouldHideEvent.ts
@@ -14,10 +14,20 @@
limitations under the License.
*/
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+
import SettingsStore from "./settings/SettingsStore";
-function memberEventDiff(ev) {
- const diff = {
+interface IDiff {
+ isMemberEvent: boolean;
+ isJoin?: boolean;
+ isPart?: boolean;
+ isDisplaynameChange?: boolean;
+ isAvatarChange?: boolean;
+}
+
+function memberEventDiff(ev: MatrixEvent): IDiff {
+ const diff: IDiff = {
isMemberEvent: ev.getType() === 'm.room.member',
};
@@ -37,7 +47,7 @@ function memberEventDiff(ev) {
return diff;
}
-export default function shouldHideEvent(ev) {
+export default function shouldHideEvent(ev: MatrixEvent): boolean {
// Wrap getValue() for readability. Calling the SettingsStore can be
// fairly resource heavy, so the checks below should avoid hitting it
// where possible.
diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts
index 4ff859d4fe..95d56bd40e 100644
--- a/src/stores/CommunityPrototypeStore.ts
+++ b/src/stores/CommunityPrototypeStore.ts
@@ -23,7 +23,7 @@ import SettingsStore from "../settings/SettingsStore";
import * as utils from "matrix-js-sdk/src/utils";
import { UPDATE_EVENT } from "./AsyncStore";
import FlairStore from "./FlairStore";
-import TagOrderStore from "./TagOrderStore";
+import GroupFilterOrderStore from "./GroupFilterOrderStore";
import GroupStore from "./GroupStore";
import dis from "../dispatcher/dispatcher";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
@@ -50,7 +50,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient {
public getSelectedCommunityId(): string {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
- return TagOrderStore.getSelectedTags()[0];
+ return GroupFilterOrderStore.getSelectedTags()[0];
}
return null; // no selection as far as this function is concerned
}
diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js
index 39177181b4..edfc0003cf 100644
--- a/src/stores/CustomRoomTagStore.js
+++ b/src/stores/CustomRoomTagStore.js
@@ -46,7 +46,7 @@ function commonPrefix(a, b) {
return "";
}
/**
- * A class for storing application state for ordering tags in the TagPanel.
+ * A class for storing application state for ordering tags in the GroupFilterPanel.
*/
class CustomRoomTagStore extends EventEmitter {
constructor() {
diff --git a/src/stores/TagOrderStore.js b/src/stores/GroupFilterOrderStore.js
similarity index 97%
rename from src/stores/TagOrderStore.js
rename to src/stores/GroupFilterOrderStore.js
index 2b72a963b0..492322146e 100644
--- a/src/stores/TagOrderStore.js
+++ b/src/stores/GroupFilterOrderStore.js
@@ -33,9 +33,9 @@ const INITIAL_STATE = {
};
/**
- * A class for storing application state for ordering tags in the TagPanel.
+ * A class for storing application state for ordering tags in the GroupFilterPanel.
*/
-class TagOrderStore extends Store {
+class GroupFilterOrderStore extends Store {
constructor() {
super(dis);
@@ -268,7 +268,7 @@ class TagOrderStore extends Store {
}
}
-if (global.singletonTagOrderStore === undefined) {
- global.singletonTagOrderStore = new TagOrderStore();
+if (global.singletonGroupFilterOrderStore === undefined) {
+ global.singletonGroupFilterOrderStore = new GroupFilterOrderStore();
}
-export default global.singletonTagOrderStore;
+export default global.singletonGroupFilterOrderStore;
diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts
new file mode 100644
index 0000000000..0485afd106
--- /dev/null
+++ b/src/stores/ModalWidgetStore.ts
@@ -0,0 +1,87 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import { ActionPayload } from "../dispatcher/payloads";
+import Modal, {IHandle, IModal} from "../Modal";
+import ModalWidgetDialog from "../components/views/dialogs/ModalWidgetDialog";
+import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
+import {IModalWidgetOpenRequestData, IModalWidgetReturnData, Widget} from "matrix-widget-api";
+
+interface IState {
+ modal?: IModal;
+ openedFromId?: string;
+}
+
+export class ModalWidgetStore extends AsyncStoreWithClient {
+ private static internalInstance = new ModalWidgetStore();
+ private modalInstance: IHandle = null;
+ private openSourceWidgetId: string = null;
+
+ private constructor() {
+ super(defaultDispatcher, {});
+ }
+
+ public static get instance(): ModalWidgetStore {
+ return ModalWidgetStore.internalInstance;
+ }
+
+ protected async onAction(payload: ActionPayload): Promise {
+ // nothing
+ }
+
+ public canOpenModalWidget = () => {
+ return !this.modalInstance;
+ };
+
+ public openModalWidget = (requestData: IModalWidgetOpenRequestData, sourceWidget: Widget) => {
+ if (this.modalInstance) return;
+ this.openSourceWidgetId = sourceWidget.id;
+ this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, {
+ widgetDefinition: {...requestData},
+ sourceWidgetId: sourceWidget.id,
+ onFinished: (success: boolean, data?: IModalWidgetReturnData) => {
+ if (!success) {
+ this.closeModalWidget(sourceWidget, { "m.exited": true });
+ } else {
+ this.closeModalWidget(sourceWidget, data);
+ }
+
+ this.openSourceWidgetId = null;
+ this.modalInstance = null;
+ },
+ });
+ };
+
+ public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
+ if (!this.modalInstance) return;
+ if (this.openSourceWidgetId === sourceWidget.id) {
+ this.openSourceWidgetId = null;
+ this.modalInstance.close();
+ this.modalInstance = null;
+
+ const sourceMessaging = WidgetMessagingStore.instance.getMessaging(sourceWidget);
+ if (!sourceMessaging) {
+ console.error("No source widget messaging for modal widget");
+ return;
+ }
+ sourceMessaging.notifyModalWidgetClose(data);
+ }
+ };
+}
+
+window.mxModalWidgetStore = ModalWidgetStore.instance;
diff --git a/src/stores/WidgetEchoStore.js b/src/stores/WidgetEchoStore.js
index 7dd093d45e..3aef1beb3e 100644
--- a/src/stores/WidgetEchoStore.js
+++ b/src/stores/WidgetEchoStore.js
@@ -55,7 +55,7 @@ class WidgetEchoStore extends EventEmitter {
const widgetId = w.getStateKey();
// If there's no echo, or the echo still has a widget present, show the *old* widget
// we don't include widgets that have changed for the same reason we don't include new ones,
- // ie. we'd need to fake matrix events to do so and therte's currently no need.
+ // ie. we'd need to fake matrix events to do so and there's currently no need.
if (!roomEchoState[widgetId] || Object.keys(roomEchoState[widgetId]).length !== 0) {
echoedWidgets.push(w);
}
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index d9cbdec76d..a8040f57de 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -16,12 +16,14 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { IWidget } from "matrix-widget-api";
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import SettingsStore from "../settings/SettingsStore";
import WidgetEchoStore from "../stores/WidgetEchoStore";
+import RoomViewStore from "../stores/RoomViewStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import WidgetUtils from "../utils/WidgetUtils";
import {SettingLevel} from "../settings/SettingLevel";
@@ -30,13 +32,9 @@ import {UPDATE_EVENT} from "./AsyncStore";
interface IState {}
-export interface IApp {
- id: string;
- type: string;
+export interface IApp extends IWidget {
roomId: string;
eventId: string;
- creatorUserId: string;
- waitForIframeLoad?: boolean;
// eslint-disable-next-line camelcase
avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
}
@@ -46,6 +44,8 @@ interface IRoomWidgets {
pinned: Record;
}
+export const MAX_PINNED = 3;
+
// TODO consolidate WidgetEchoStore into this
// TODO consolidate ActiveWidgetStore into this
export default class WidgetStore extends AsyncStoreWithClient {
@@ -68,7 +68,7 @@ export default class WidgetStore extends AsyncStoreWithClient {
private initRoom(roomId: string) {
if (!this.roomMap.has(roomId)) {
this.roomMap.set(roomId, {
- pinned: {},
+ pinned: {}, // ordered
widgets: [],
});
}
@@ -122,6 +122,15 @@ export default class WidgetStore extends AsyncStoreWithClient {
if (!room) return;
const roomInfo = this.roomMap.get(room.roomId);
roomInfo.widgets = [];
+
+ // first clean out old widgets from the map which originate from this room
+ // otherwise we are out of sync with the rest of the app with stale widget events during removal
+ Array.from(this.widgetMap.values()).forEach(app => {
+ if (app.roomId === room.roomId) {
+ this.widgetMap.delete(app.id);
+ }
+ });
+
this.generateApps(room).forEach(app => {
this.widgetMap.set(app.id, app);
roomInfo.widgets.push(app);
@@ -156,27 +165,34 @@ export default class WidgetStore extends AsyncStoreWithClient {
public isPinned(widgetId: string) {
const roomId = this.getRoomId(widgetId);
- const roomInfo = this.getRoom(roomId);
-
- let pinned = roomInfo && roomInfo.pinned[widgetId];
- // Jitsi widgets should be pinned by default
- const widget = this.widgetMap.get(widgetId);
- if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true;
- return pinned;
+ return !!this.getPinnedApps(roomId).find(w => w.id === widgetId);
}
public canPin(widgetId: string) {
- // only allow pinning up to a max of two as we do not yet have grid splits
- // the only case it will go to three is if you have two and then a Jitsi gets added
const roomId = this.getRoomId(widgetId);
- const roomInfo = this.getRoom(roomId);
- return roomInfo && Object.keys(roomInfo.pinned).filter(k => {
- return roomInfo.pinned[k] && roomInfo.widgets.some(app => app.id === k);
- }).length < 2;
+ return this.getPinnedApps(roomId).length < MAX_PINNED;
}
public pinWidget(widgetId: string) {
+ const roomId = this.getRoomId(widgetId);
+ const roomInfo = this.getRoom(roomId);
+ if (!roomInfo) return;
+
+ // When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct
+ const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]);
+ autoPinned.forEach(app => {
+ this.setPinned(app.id, true);
+ });
+
this.setPinned(widgetId, true);
+
+ // Show the apps drawer upon the user pinning a widget
+ if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
+ defaultDispatcher.dispatch({
+ action: "appsDrawer",
+ show: true,
+ })
+ }
}
public unpinWidget(widgetId: string) {
@@ -187,6 +203,10 @@ export default class WidgetStore extends AsyncStoreWithClient {
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId);
if (!roomInfo) return;
+ if (roomInfo.pinned[widgetId] === false && value) {
+ // delete this before write to maintain the correct object insertion order
+ delete roomInfo.pinned[widgetId];
+ }
roomInfo.pinned[widgetId] = value;
// Clean up the pinned record
@@ -201,13 +221,61 @@ export default class WidgetStore extends AsyncStoreWithClient {
this.emit(UPDATE_EVENT);
}
- public getApps(room: Room, pinned?: boolean): IApp[] {
- const roomInfo = this.getRoom(room.roomId);
- if (!roomInfo) return [];
- if (pinned) {
- return roomInfo.widgets.filter(app => this.isPinned(app.id));
+ public movePinnedWidget(widgetId: string, delta: 1 | -1) {
+ // TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away
+ const roomId = this.getRoomId(widgetId);
+ const roomInfo = this.getRoom(roomId);
+ if (!roomInfo || roomInfo.pinned[widgetId] === false) return;
+
+ const pinnedApps = this.getPinnedApps(roomId).map(app => app.id);
+ const i = pinnedApps.findIndex(id => id === widgetId);
+
+ if (delta > 0) {
+ pinnedApps.splice(i, 2, pinnedApps[i + 1], pinnedApps[i]);
+ } else {
+ pinnedApps.splice(i - 1, 2, pinnedApps[i], pinnedApps[i - 1]);
}
- return roomInfo.widgets;
+
+ const reorderedPinned: IRoomWidgets["pinned"] = {};
+ pinnedApps.forEach(id => {
+ reorderedPinned[id] = true;
+ });
+ Object.keys(roomInfo.pinned).forEach(id => {
+ if (reorderedPinned[id] === undefined) {
+ reorderedPinned[id] = roomInfo.pinned[id];
+ }
+ });
+ roomInfo.pinned = reorderedPinned;
+
+ SettingsStore.setValue("Widgets.pinned", roomId, SettingLevel.ROOM_ACCOUNT, roomInfo.pinned);
+ this.emit(roomId);
+ this.emit(UPDATE_EVENT);
+ }
+
+ public getPinnedApps(roomId: string): IApp[] {
+ // returns the apps in the order they were pinned with, up to the maximum
+ const roomInfo = this.getRoom(roomId);
+ if (!roomInfo) return [];
+
+ // Show Jitsi widgets even if the user already had the maximum pinned, instead of their latest pinned,
+ // except if the user already explicitly unpinned the Jitsi widget
+ const priorityWidget = roomInfo.widgets.find(widget => {
+ return roomInfo.pinned[widget.id] === undefined && WidgetType.JITSI.matches(widget.type);
+ });
+
+ const order = Object.keys(roomInfo.pinned).filter(k => roomInfo.pinned[k]);
+ let apps = order.map(wId => this.widgetMap.get(wId)).filter(Boolean);
+ apps = apps.slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
+ if (priorityWidget) {
+ apps.push(priorityWidget);
+ }
+
+ return apps;
+ }
+
+ public getApps(roomId: string): IApp[] {
+ const roomInfo = this.getRoom(roomId);
+ return roomInfo?.widgets || [];
}
public doesRoomHaveConference(room: Room): boolean {
diff --git a/src/stores/room-list/TagWatcher.ts b/src/stores/room-list/TagWatcher.ts
index dc445d10ba..7e809283e9 100644
--- a/src/stores/room-list/TagWatcher.ts
+++ b/src/stores/room-list/TagWatcher.ts
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { RoomListStoreClass } from "./RoomListStore";
-import TagOrderStore from "../TagOrderStore";
+import GroupFilterOrderStore from "../GroupFilterOrderStore";
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
@@ -26,12 +26,12 @@ export class TagWatcher {
private filters = new Map();
constructor(private store: RoomListStoreClass) {
- TagOrderStore.addListener(this.onTagsUpdated);
+ GroupFilterOrderStore.addListener(this.onTagsUpdated);
}
private onTagsUpdated = () => {
const lastTags = Array.from(this.filters.keys());
- const newTags = TagOrderStore.getSelectedTags();
+ const newTags = GroupFilterOrderStore.getSelectedTags();
if (arrayHasDiff(lastTags, newTags)) {
// Selected tags changed, do some filtering
diff --git a/src/stores/room-list/previews/ReactionEventPreview.ts b/src/stores/room-list/previews/ReactionEventPreview.ts
index 95cdc01c66..0012cf2f75 100644
--- a/src/stores/room-list/previews/ReactionEventPreview.ts
+++ b/src/stores/room-list/previews/ReactionEventPreview.ts
@@ -27,7 +27,13 @@ export class ReactionEventPreview implements IPreview {
const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms");
const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all");
- if (!showAll && (!showDms || DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) return null;
+ // If we're not showing all reactions, see if we're showing DMs instead
+ if (!showAll) {
+ // If we're not showing reactions on DMs, or we are and the room isn't a DM, skip
+ if (!(showDms && DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) {
+ return null;
+ }
+ }
const relation = event.getRelation();
if (!relation) return null; // invalid reaction (probably redacted)
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 1eb4f9cd27..0ae835b603 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -17,6 +17,8 @@
import { Room } from "matrix-js-sdk/src/models/room";
import {
ClientWidgetApi,
+ IGetOpenIDActionRequest,
+ IGetOpenIDActionResponseData,
IStickerActionRequest,
IStickyActionRequest,
ITemplateParams,
@@ -25,9 +27,12 @@ import {
IWidgetApiRequestEmptyData,
IWidgetData,
MatrixCapabilities,
+ OpenIDRequestState,
runTemplate,
Widget,
+ WidgetApiToWidgetAction,
WidgetApiFromWidgetAction,
+ IModalWidgetOpenRequest,
} from "matrix-widget-api";
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
import { EventEmitter } from "events";
@@ -43,6 +48,9 @@ import ActiveWidgetStore from "../ActiveWidgetStore";
import { objectShallowClone } from "../../utils/objects";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ElementWidgetActions } from "./ElementWidgetActions";
+import Modal from "../../Modal";
+import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
+import {ModalWidgetStore} from "../ModalWidgetStore";
// TODO: Destroy all of this code
@@ -68,12 +76,22 @@ class ElementWidget extends Widget {
if (WidgetType.JITSI.matches(this.type)) {
return WidgetUtils.getLocalJitsiWrapperUrl({
forLocalRender: true,
- auth: super.rawData?.auth, // this.rawData can call templateUrl, do this to prevent looping
+ auth: super.rawData?.auth as string, // this.rawData can call templateUrl, do this to prevent looping
});
}
return super.templateUrl;
}
+ public get popoutTemplateUrl(): string {
+ if (WidgetType.JITSI.matches(this.type)) {
+ return WidgetUtils.getLocalJitsiWrapperUrl({
+ forLocalRender: false, // The only important difference between this and templateUrl()
+ auth: super.rawData?.auth as string,
+ });
+ }
+ return this.templateUrl; // use this instead of super to ensure we get appropriate templating
+ }
+
public get rawData(): IWidgetData {
let conferenceId = super.rawData['conferenceId'];
if (conferenceId === undefined) {
@@ -94,8 +112,8 @@ class ElementWidget extends Widget {
};
}
- public getCompleteUrl(params: ITemplateParams): string {
- return runTemplate(this.templateUrl, {
+ public getCompleteUrl(params: ITemplateParams, asPopout=false): string {
+ return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, {
// we need to supply a whole widget to the template, but don't have
// easy access to the definition the superclass is using, so be sad
// and gutwrench it.
@@ -109,7 +127,7 @@ class ElementWidget extends Widget {
export class StopGapWidget extends EventEmitter {
private messaging: ClientWidgetApi;
- private mockWidget: Widget;
+ private mockWidget: ElementWidget;
private scalarToken: string;
constructor(private appTileProps: IAppTileProps) {
@@ -133,42 +151,43 @@ export class StopGapWidget extends EventEmitter {
* The URL to use in the iframe
*/
public get embedUrl(): string {
- const templated = this.mockWidget.getCompleteUrl({
- currentRoomId: RoomViewStore.getRoomId(),
- currentUserId: MatrixClientPeg.get().getUserId(),
- userDisplayName: OwnProfileStore.instance.displayName,
- userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
- });
-
- // Add in some legacy support sprinkles
- // TODO: Replace these with proper widget params
- // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
- const parsed = new URL(templated);
- parsed.searchParams.set('widgetId', this.mockWidget.id);
- parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
-
- // Give the widget a scalar token if we're supposed to (more legacy)
- // TODO: Stop doing this
- if (this.scalarToken) {
- parsed.searchParams.set('scalar_token', this.scalarToken);
- }
-
- // Replace the encoded dollar signs back to dollar signs. They have no special meaning
- // in HTTP, but URL parsers encode them anyways.
- return parsed.toString().replace(/%24/g, '$');
+ return this.runUrlTemplate({asPopout: false});
}
/**
* The URL to use in the popout
*/
public get popoutUrl(): string {
- if (WidgetType.JITSI.matches(this.mockWidget.type)) {
- return WidgetUtils.getLocalJitsiWrapperUrl({
- forLocalRender: false,
- auth: this.mockWidget.rawData?.auth,
- });
+ return this.runUrlTemplate({asPopout: true});
+ }
+
+ private runUrlTemplate(opts = {asPopout: false}): string {
+ const templated = this.mockWidget.getCompleteUrl({
+ currentRoomId: RoomViewStore.getRoomId(),
+ currentUserId: MatrixClientPeg.get().getUserId(),
+ userDisplayName: OwnProfileStore.instance.displayName,
+ userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
+ }, opts?.asPopout);
+
+ const parsed = new URL(templated);
+
+ // Add in some legacy support sprinkles (for non-popout widgets)
+ // TODO: Replace these with proper widget params
+ // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
+ if (!opts?.asPopout) {
+ parsed.searchParams.set('widgetId', this.mockWidget.id);
+ parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
+
+ // Give the widget a scalar token if we're supposed to (more legacy)
+ // TODO: Stop doing this
+ if (this.scalarToken) {
+ parsed.searchParams.set('scalar_token', this.scalarToken);
+ }
}
- return this.embedUrl;
+
+ // Replace the encoded dollar signs back to dollar signs. They have no special meaning
+ // in HTTP, but URL parsers encode them anyways.
+ return parsed.toString().replace(/%24/g, '$');
}
public get isManagedByManager(): boolean {
@@ -179,12 +198,81 @@ export class StopGapWidget extends EventEmitter {
return !!this.messaging;
}
+ private get widgetId() {
+ return this.messaging.widget.id;
+ }
+
+ private onOpenIdReq = async (ev: CustomEvent) => {
+ ev.preventDefault();
+
+ const rawUrl = this.appTileProps.app.url;
+ const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, rawUrl, this.appTileProps.userWidget);
+
+ const settings = SettingsStore.getValue("widgetOpenIDPermissions");
+ if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
+ this.messaging.transport.reply(ev.detail, {
+ state: OpenIDRequestState.Blocked,
+ });
+ return;
+ }
+ if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
+ const credentials = await MatrixClientPeg.get().getOpenIdToken();
+ this.messaging.transport.reply(ev.detail, {
+ state: OpenIDRequestState.Allowed,
+ ...credentials,
+ });
+ return;
+ }
+
+ // Confirm that we received the request
+ this.messaging.transport.reply(ev.detail, {
+ state: OpenIDRequestState.PendingUserConfirmation,
+ });
+
+ // Actually ask for permission to send the user's data
+ Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
+ widgetUrl: rawUrl.substr(0, rawUrl.lastIndexOf("?")),
+ widgetId: this.widgetId,
+ isUserWidget: this.appTileProps.userWidget,
+
+ onFinished: async (confirm) => {
+ const responseBody: IGetOpenIDActionResponseData = {
+ state: confirm ? OpenIDRequestState.Allowed : OpenIDRequestState.Blocked,
+ original_request_id: ev.detail.requestId, // eslint-disable-line camelcase
+ };
+ if (confirm) {
+ const credentials = await MatrixClientPeg.get().getOpenIdToken();
+ Object.assign(responseBody, credentials);
+ }
+ this.messaging.transport.send(WidgetApiToWidgetAction.OpenIDCredentials, responseBody).catch(error => {
+ console.error("Failed to send OpenID credentials: ", error);
+ });
+ },
+ });
+ };
+
+ private onOpenModal = async (ev: CustomEvent) => {
+ ev.preventDefault();
+ if (ModalWidgetStore.instance.canOpenModalWidget()) {
+ ModalWidgetStore.instance.openModalWidget(ev.detail.data, this.mockWidget);
+ this.messaging.transport.reply(ev.detail, {}); // ack
+ } else {
+ this.messaging.transport.reply(ev.detail, {
+ error: {
+ message: "Unable to open modal at this time",
+ },
+ })
+ }
+ };
+
public start(iframe: HTMLIFrameElement) {
if (this.started) return;
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
- this.messaging.addEventListener("preparing", () => this.emit("preparing"));
- this.messaging.addEventListener("ready", () => this.emit("ready"));
+ this.messaging.on("preparing", () => this.emit("preparing"));
+ this.messaging.on("ready", () => this.emit("ready"));
+ this.messaging.on(`action:${WidgetApiFromWidgetAction.GetOpenIDCredentials}`, this.onOpenIdReq);
+ this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
if (!this.appTileProps.userWidget && this.appTileProps.room) {
@@ -192,7 +280,7 @@ export class StopGapWidget extends EventEmitter {
}
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
- this.messaging.addEventListener("action:set_always_on_screen",
+ this.messaging.on("action:set_always_on_screen",
(ev: CustomEvent) => {
if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
@@ -202,7 +290,7 @@ export class StopGapWidget extends EventEmitter {
},
);
} else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
- this.messaging.addEventListener(`action:${ElementWidgetActions.OpenIntegrationManager}`,
+ this.messaging.on(`action:${ElementWidgetActions.OpenIntegrationManager}`,
(ev: CustomEvent) => {
// Acknowledge first
ev.preventDefault();
@@ -236,7 +324,7 @@ export class StopGapWidget extends EventEmitter {
// TODO: Replace this event listener with appropriate driver functionality once the API
// establishes a sane way to send events back and forth.
- this.messaging.addEventListener(`action:${WidgetApiFromWidgetAction.SendSticker}`,
+ this.messaging.on(`action:${WidgetApiFromWidgetAction.SendSticker}`,
(ev: CustomEvent) => {
// Acknowledge first
ev.preventDefault();
@@ -275,8 +363,8 @@ export class StopGapWidget extends EventEmitter {
}
}
- public stop() {
- if (ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
+ public stop(opts = {forceDestroy: false}) {
+ if (!opts?.forceDestroy && ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
console.log("Skipping destroy - persistent widget");
return;
}
diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts
index 5aa030e497..ade7dfe3f0 100644
--- a/src/toasts/SetupEncryptionToast.ts
+++ b/src/toasts/SetupEncryptionToast.ts
@@ -22,6 +22,7 @@ import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEnc
import { accessSecretStorage } from "../SecurityManager";
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
+import SecurityCustomisations from "../customisations/Security";
const TOAST_KEY = "setupencryption";
@@ -78,6 +79,10 @@ const onReject = () => {
};
export const showToast = (kind: Kind) => {
+ if (SecurityCustomisations.setupEncryptionNeeded?.(kind)) {
+ return;
+ }
+
const onAccept = async () => {
if (kind === Kind.VERIFY_THIS_SESSION) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog,
diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js
index 512946828b..fd12a454f6 100644
--- a/src/utils/ResizeNotifier.js
+++ b/src/utils/ResizeNotifier.js
@@ -40,10 +40,12 @@ export default class ResizeNotifier extends EventEmitter {
startResizing() {
this._isResizing = true;
+ this.emit("isResizing", true);
}
stopResizing() {
this._isResizing = false;
+ this.emit("isResizing", false);
}
_noisyMiddlePanel() {
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.ts
similarity index 86%
rename from src/utils/WidgetUtils.js
rename to src/utils/WidgetUtils.ts
index 6cc95efb25..526c2d5ce7 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.ts
@@ -1,7 +1,6 @@
/*
-Copyright 2017 Vector Creations Ltd
-Copyright 2018 New Vector Ltd
Copyright 2019 Travis Ralston
+Copyright 2017 - 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,15 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import * as url from "url";
+
import {MatrixClientPeg} from '../MatrixClientPeg';
import SdkConfig from "../SdkConfig";
import dis from '../dispatcher/dispatcher';
-import * as url from "url";
import WidgetEchoStore from '../stores/WidgetEchoStore';
-
-// How long we wait for the state event echo to come back from the server
-// before waitFor[Room/User]Widget rejects its promise
-const WIDGET_WAIT_TIME = 20000;
import SettingsStore from "../settings/SettingsStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers";
@@ -32,7 +28,21 @@ import {Room} from "matrix-js-sdk/src/models/room";
import {WidgetType} from "../widgets/WidgetType";
import {objectClone} from "./objects";
import {_t} from "../languageHandler";
-import {MatrixCapabilities} from "matrix-widget-api";
+import {Capability, IWidgetData, MatrixCapabilities} from "matrix-widget-api";
+import {IApp} from "../stores/WidgetStore"; // TODO @@
+
+// How long we wait for the state event echo to come back from the server
+// before waitFor[Room/User]Widget rejects its promise
+const WIDGET_WAIT_TIME = 20000;
+
+export interface IWidgetEvent {
+ id: string;
+ type: string;
+ sender: string;
+ // eslint-disable-next-line camelcase
+ state_key: string;
+ content: Partial;
+}
export default class WidgetUtils {
/* Returns true if user is able to send state events to modify widgets in this room
@@ -41,7 +51,7 @@ export default class WidgetUtils {
* @return Boolean -- true if the user can modify widgets in this room
* @throws Error -- specifies the error reason
*/
- static canUserModifyWidgets(roomId) {
+ static canUserModifyWidgets(roomId: string): boolean {
if (!roomId) {
console.warn('No room ID specified');
return false;
@@ -80,7 +90,7 @@ export default class WidgetUtils {
* @param {[type]} testUrlString URL to check
* @return {Boolean} True if specified URL is a scalar URL
*/
- static isScalarUrl(testUrlString) {
+ static isScalarUrl(testUrlString: string): boolean {
if (!testUrlString) {
console.error('Scalar URL check failed. No URL specified');
return false;
@@ -123,7 +133,7 @@ export default class WidgetUtils {
* @returns {Promise} that resolves when the widget is in the
* requested state according to the `add` param
*/
- static waitForUserWidget(widgetId, add) {
+ static waitForUserWidget(widgetId: string, add: boolean): Promise {
return new Promise((resolve, reject) => {
// Tests an account data event, returning true if it's in the state
// we're waiting for it to be in
@@ -170,7 +180,7 @@ export default class WidgetUtils {
* @returns {Promise} that resolves when the widget is in the
* requested state according to the `add` param
*/
- static waitForRoomWidget(widgetId, roomId, add) {
+ static waitForRoomWidget(widgetId: string, roomId: string, add: boolean): Promise {
return new Promise((resolve, reject) => {
// Tests a list of state events, returning true if it's in the state
// we're waiting for it to be in
@@ -213,7 +223,13 @@ export default class WidgetUtils {
});
}
- static setUserWidget(widgetId, widgetType: WidgetType, widgetUrl, widgetName, widgetData) {
+ static setUserWidget(
+ widgetId: string,
+ widgetType: WidgetType,
+ widgetUrl: string,
+ widgetName: string,
+ widgetData: IWidgetData,
+ ) {
const content = {
type: widgetType.preferred,
url: widgetUrl,
@@ -257,7 +273,14 @@ export default class WidgetUtils {
});
}
- static setRoomWidget(roomId, widgetId, widgetType: WidgetType, widgetUrl, widgetName, widgetData) {
+ static setRoomWidget(
+ roomId: string,
+ widgetId: string,
+ widgetType?: WidgetType,
+ widgetUrl?: string,
+ widgetName?: string,
+ widgetData?: object,
+ ) {
let content;
const addingWidget = Boolean(widgetUrl);
@@ -307,7 +330,7 @@ export default class WidgetUtils {
* Get user specific widgets (not linked to a specific room)
* @return {object} Event content object containing current / active user widgets
*/
- static getUserWidgets() {
+ static getUserWidgets(): Record {
const client = MatrixClientPeg.get();
if (!client) {
throw new Error('User not logged in');
@@ -323,7 +346,7 @@ export default class WidgetUtils {
* Get user specific widgets (not linked to a specific room) as an array
* @return {[object]} Array containing current / active user widgets
*/
- static getUserWidgetsArray() {
+ static getUserWidgetsArray(): IWidgetEvent[] {
return Object.values(WidgetUtils.getUserWidgets());
}
@@ -331,7 +354,7 @@ export default class WidgetUtils {
* Get active stickerpicker widgets (stickerpickers are user widgets by nature)
* @return {[object]} Array containing current / active stickerpicker widgets
*/
- static getStickerpickerWidgets() {
+ static getStickerpickerWidgets(): IWidgetEvent[] {
const widgets = WidgetUtils.getUserWidgetsArray();
return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker");
}
@@ -340,12 +363,12 @@ export default class WidgetUtils {
* Get all integration manager widgets for this user.
* @returns {Object[]} An array of integration manager user widgets.
*/
- static getIntegrationManagerWidgets() {
+ static getIntegrationManagerWidgets(): IWidgetEvent[] {
const widgets = WidgetUtils.getUserWidgetsArray();
return widgets.filter(w => w.content && w.content.type === "m.integration_manager");
}
- static getRoomWidgetsOfType(room: Room, type: WidgetType) {
+ static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] {
const widgets = WidgetUtils.getRoomWidgets(room);
return (widgets || []).filter(w => {
const content = w.getContent();
@@ -353,14 +376,14 @@ export default class WidgetUtils {
});
}
- static removeIntegrationManagerWidgets() {
+ static removeIntegrationManagerWidgets(): Promise {
const client = MatrixClientPeg.get();
if (!client) {
throw new Error('User not logged in');
}
const widgets = client.getAccountData('m.widgets');
if (!widgets) return;
- const userWidgets = widgets.getContent() || {};
+ const userWidgets: IWidgetEvent[] = widgets.getContent() || {};
Object.entries(userWidgets).forEach(([key, widget]) => {
if (widget.content && widget.content.type === "m.integration_manager") {
delete userWidgets[key];
@@ -369,7 +392,7 @@ export default class WidgetUtils {
return client.setAccountData('m.widgets', userWidgets);
}
- static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) {
+ static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string): Promise {
return WidgetUtils.setUserWidget(
"integration_manager_" + (new Date().getTime()),
WidgetType.INTEGRATION_MANAGER,
@@ -383,14 +406,14 @@ export default class WidgetUtils {
* Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
* @return {Promise} Resolves on account data updated
*/
- static removeStickerpickerWidgets() {
+ static removeStickerpickerWidgets(): Promise {
const client = MatrixClientPeg.get();
if (!client) {
throw new Error('User not logged in');
}
const widgets = client.getAccountData('m.widgets');
if (!widgets) return;
- const userWidgets = widgets.getContent() || {};
+ const userWidgets: Record = widgets.getContent() || {};
Object.entries(userWidgets).forEach(([key, widget]) => {
if (widget.content && widget.content.type === 'm.stickerpicker') {
delete userWidgets[key];
@@ -399,7 +422,13 @@ export default class WidgetUtils {
return client.setAccountData('m.widgets', userWidgets);
}
- static makeAppConfig(appId, app, senderUserId, roomId, eventId) {
+ static makeAppConfig(
+ appId: string,
+ app: Partial,
+ senderUserId: string,
+ roomId: string | null,
+ eventId: string,
+ ): IApp {
if (!senderUserId) {
throw new Error("Widgets must be created by someone - provide a senderUserId");
}
@@ -410,10 +439,10 @@ export default class WidgetUtils {
app.eventId = eventId;
app.name = app.name || app.type;
- return app;
+ return app as IApp;
}
- static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
+ static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] {
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
@@ -428,7 +457,7 @@ export default class WidgetUtils {
return capWhitelist;
}
- static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) {
+ static getWidgetSecurityKey(widgetId: string, widgetUrl: string, isUserWidget: boolean): string {
let widgetLocation = ActiveWidgetStore.getRoomId(widgetId);
if (isUserWidget) {
@@ -449,7 +478,7 @@ export default class WidgetUtils {
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
}
- static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string}={}) {
+ static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
const queryStringParts = [
'conferenceDomain=$domain',
@@ -459,13 +488,14 @@ export default class WidgetUtils {
'avatarUrl=$matrix_avatar_url',
'userId=$matrix_user_id',
'roomId=$matrix_room_id',
+ 'theme=$theme',
];
if (opts.auth) {
queryStringParts.push(`auth=${opts.auth}`);
}
const queryString = queryStringParts.join('&');
- let baseUrl = window.location;
+ let baseUrl = window.location.href;
if (window.location.protocol !== "https:" && !opts.forLocalRender) {
// Use an external wrapper if we're not locally rendering the widget. This is usually
// the URL that will end up in the widget event, so we want to make sure it's relatively
@@ -478,15 +508,15 @@ export default class WidgetUtils {
return url.href;
}
- static getWidgetName(app) {
+ static getWidgetName(app?: IApp): string {
return app?.name?.trim() || _t("Unknown App");
}
- static getWidgetDataTitle(app) {
+ static getWidgetDataTitle(app?: IApp): string {
return app?.data?.title?.trim() || "";
}
- static editWidget(room, app) {
+ static editWidget(room: Room, app: IApp): void {
// TODO: Open the right manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(room, 'type_' + app.type, app.id);
@@ -494,4 +524,16 @@ export default class WidgetUtils {
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
}
}
+
+ static isManagedByManager(app) {
+ if (WidgetUtils.isScalarUrl(app.url)) {
+ const managers = IntegrationManagers.sharedInstance();
+ if (managers.hasManager()) {
+ // TODO: Pick the right manager for the widget
+ const defaultManager = managers.getPrimaryManager();
+ return WidgetUtils.isScalarUrl(defaultManager.apiUrl);
+ }
+ }
+ return false;
+ }
}
diff --git a/src/utils/colour.js b/src/utils/colour.js
deleted file mode 100644
index 2ec904c0d3..0000000000
--- a/src/utils/colour.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
-Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-
-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.
-*/
-
-export function hueToRGB(h, s, l) {
- const c = s * (1 - Math.abs(2 * l - 1));
- const x = c * (1 - Math.abs((h / 60) % 2 - 1));
- const m = l - c / 2;
-
- let r = 0;
- let g = 0;
- let b = 0;
-
- if (0 <= h && h < 60) {
- r = c;
- g = x;
- b = 0;
- } else if (60 <= h && h < 120) {
- r = x;
- g = c;
- b = 0;
- } else if (120 <= h && h < 180) {
- r = 0;
- g = c;
- b = x;
- } else if (180 <= h && h < 240) {
- r = 0;
- g = x;
- b = c;
- } else if (240 <= h && h < 300) {
- r = x;
- g = 0;
- b = c;
- } else if (300 <= h && h < 360) {
- r = c;
- g = 0;
- b = x;
- }
-
- return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
-}
-
-
-export function textToHtmlRainbow(str) {
- const frequency = 360 / str.length;
-
- return Array.from(str).map((c, i) => {
- const [r, g, b] = hueToRGB(i * frequency, 1.0, 0.5);
- return '' + c + '';
- }).join("");
-}
diff --git a/src/utils/colour.ts b/src/utils/colour.ts
new file mode 100644
index 0000000000..10c18dbfe7
--- /dev/null
+++ b/src/utils/colour.ts
@@ -0,0 +1,88 @@
+/*
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+
+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.
+*/
+
+export function textToHtmlRainbow(str: string): string {
+ const frequency = (2 * Math.PI) / str.length;
+
+ return Array.from(str)
+ .map((c, i) => {
+ if (c === " ") {
+ return c;
+ }
+ const [a, b] = generateAB(i * frequency, 1);
+ const [red, green, blue] = labToRGB(75, a, b);
+ return (
+ '' +
+ c +
+ ""
+ );
+ })
+ .join("");
+}
+
+function generateAB(hue: number, chroma: number): [number, number] {
+ const a = chroma * 127 * Math.cos(hue);
+ const b = chroma * 127 * Math.sin(hue);
+
+ return [a, b];
+}
+
+function labToRGB(l: number, a: number, b: number): [number, number, number] {
+ // https://en.wikipedia.org/wiki/CIELAB_color_space#Reverse_transformation
+ // https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_(CIE_XYZ_to_sRGB)
+
+ // Convert CIELAB to CIEXYZ (D65)
+ let y = (l + 16) / 116;
+ const x = adjustXYZ(y + a / 500) * 0.9505;
+ const z = adjustXYZ(y - b / 200) * 1.089;
+
+ y = adjustXYZ(y);
+
+ // Linear transformation from CIEXYZ to RGB
+ const red = 3.24096994 * x - 1.53738318 * y - 0.49861076 * z;
+ const green = -0.96924364 * x + 1.8759675 * y + 0.04155506 * z;
+ const blue = 0.05563008 * x - 0.20397696 * y + 1.05697151 * z;
+
+ return [adjustRGB(red), adjustRGB(green), adjustRGB(blue)];
+}
+
+function adjustXYZ(v: number): number {
+ if (v > 0.2069) {
+ return Math.pow(v, 3);
+ }
+ return 0.1284 * v - 0.01771;
+}
+
+function gammaCorrection(v: number): number {
+ // Non-linear transformation to sRGB
+ if (v <= 0.0031308) {
+ return 12.92 * v;
+ }
+ return 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
+}
+
+function adjustRGB(v: number): number {
+ const corrected = gammaCorrection(v);
+
+ // Limits number between 0 and 1
+ const limited = Math.min(Math.max(corrected, 0), 1);
+
+ return Math.round(limited * 255);
+}
diff --git a/test/UserActivity-test.js b/test/UserActivity-test.js
index 1b0fbafb48..51fb720bce 100644
--- a/test/UserActivity-test.js
+++ b/test/UserActivity-test.js
@@ -64,7 +64,7 @@ describe('UserActivity', function() {
it('should not consider user active after activity if no window focus', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(false);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
expect(userActivity.userActiveNow()).toBe(false);
expect(userActivity.userActiveRecently()).toBe(false);
});
@@ -72,7 +72,7 @@ describe('UserActivity', function() {
it('should consider user active shortly after activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
expect(userActivity.userActiveNow()).toBe(true);
expect(userActivity.userActiveRecently()).toBe(true);
clock.tick(200);
@@ -83,7 +83,7 @@ describe('UserActivity', function() {
it('should consider user not active after 10s of no activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(10000);
expect(userActivity.userActiveNow()).toBe(false);
});
@@ -91,7 +91,7 @@ describe('UserActivity', function() {
it('should consider user passive after 10s of no activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(10000);
expect(userActivity.userActiveRecently()).toBe(true);
});
@@ -99,7 +99,7 @@ describe('UserActivity', function() {
it('should not consider user passive after 10s if window un-focused', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(10000);
fakeDocument.hasFocus = jest.fn().mockReturnValue(false);
@@ -111,7 +111,7 @@ describe('UserActivity', function() {
it('should not consider user passive after 3 mins', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(3 * 60 * 1000);
expect(userActivity.userActiveRecently()).toBe(false);
@@ -120,11 +120,11 @@ describe('UserActivity', function() {
it('should extend timer on activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(1 * 60 * 1000);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(1 * 60 * 1000);
- userActivity._onUserActivity({});
+ userActivity.onUserActivity({});
clock.tick(1 * 60 * 1000);
expect(userActivity.userActiveRecently()).toBe(true);
diff --git a/yarn.lock b/yarn.lock
index dc4e80013c..db5699cc21 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4155,6 +4155,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+events@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
+ integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
+
except@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/except/-/except-0.1.3.tgz#98261c91958551536b44482238e9783fb73d292a"
@@ -6508,8 +6513,8 @@ mathml-tag-names@^2.0.1:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
- version "8.4.1"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a727da9193e0ccb2fa8d7c3e8e321916f6717190"
+ version "8.5.0"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/9f713781cdfea2349115ffaac2d665e8b07fd5dc"
dependencies:
"@babel/runtime" "^7.11.2"
another-json "^0.2.0"
@@ -6534,10 +6539,12 @@ matrix-react-test-utils@^0.2.2:
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
-matrix-widget-api@^0.1.0-beta.3:
- version "0.1.0-beta.3"
- resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.3.tgz#356965ca357172ee056e3fd86fd96879b059a114"
- integrity sha512-j7nxdhLQfdU6snsdBA29KQR0DmT8/vl6otOvGqPCV0OCHpq1312cP79Eg4JzJKIFI3A76Qha3nYx6G9/aapwXg==
+matrix-widget-api@^0.1.0-beta.5:
+ version "0.1.0-beta.5"
+ resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.5.tgz#dd7f24a177aa590d812bd4e92e2c3ac225c5557e"
+ integrity sha512-J3GBJtVMFuEM/EWFylc0IlkPjdgmWxrkGYPaZ0LSmxp+OlNJxYfnWPR6F6HveW+Z8C1i0vq+BTueofSqKv2zDg==
+ dependencies:
+ events "^3.2.0"
mdast-util-compact@^1.0.0:
version "1.0.4"