Merge branch 'develop' into travis/voice/countdown

This commit is contained in:
Travis Ralston 2021-04-16 10:00:11 -06:00
commit f955f33071
22 changed files with 206 additions and 140 deletions

View file

@ -1,7 +1,7 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. # autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/Markdown.js src/Markdown.js
src/Velociraptor.js src/NodeAnimator.js
src/components/structures/RoomDirectory.js src/components/structures/RoomDirectory.js
src/components/views/rooms/MemberList.js src/components/views/rooms/MemberList.js
src/ratelimitedfunc.js src/ratelimitedfunc.js

View file

@ -102,7 +102,6 @@
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2", "text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0", "url": "^0.11.0",
"velocity-animate": "^2.0.6",
"what-input": "^5.2.10", "what-input": "^5.2.10",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },

View file

@ -28,6 +28,16 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e
:root { :root {
font-size: 10px; font-size: 10px;
--transition-short: .1s;
--transition-standard: .3s;
}
@media (prefers-reduced-motion) {
:root {
--transition-short: 0;
--transition-standard: 0;
}
} }
html { html {

View file

@ -21,6 +21,5 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
overflow-y: hidden;
} }
} }

View file

@ -117,6 +117,32 @@ limitations under the License.
.mx_UserMenu_headerButtons { .mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work. // No special styles: the rest of the layout happens to make it work.
} }
.mx_UserMenu_dnd {
width: 24px;
height: 24px;
margin-right: 8px;
position: relative;
&::before {
content: '';
position: absolute;
width: 24px;
height: 24px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $muted-fg-color;
}
&.mx_UserMenu_dnd_noisy::before {
mask-image: url('$(res)/img/element-icons/notifications.svg');
}
&.mx_UserMenu_dnd_muted::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
}
} }
&.mx_UserMenu_minimized { &.mx_UserMenu_minimized {

View file

@ -283,6 +283,10 @@ $left-gutter: 64px;
display: inline-block; display: inline-block;
height: $font-14px; height: $font-14px;
width: $font-14px; width: $font-14px;
transition:
left var(--transition-short) ease-out,
top var(--transition-standard) ease-out;
} }
.mx_EventTile_readAvatarRemainder { .mx_EventTile_readAvatarRemainder {

View file

@ -53,7 +53,8 @@ limitations under the License.
font-size: $font-14px; font-size: $font-14px;
&::before { &::before {
// TODO: @@ TravisR: Animate animation: recording-pulse 2s infinite;
content: ''; content: '';
background-color: $voice-record-live-circle-color; background-color: $voice-record-live-circle-color;
width: 10px; width: 10px;
@ -74,3 +75,26 @@ limitations under the License.
width: 42px; // we're not using a monospace font, so fake it width: 42px; // we're not using a monospace font, so fake it
} }
} }
// The keyframes are slightly weird here to help make a ramping/punch effect
// for the recording dot. We start and end at 100% opacity to help make the
// dot feel a bit like a real lamp that is blinking: the animation ends up
// spending a lot of its time showing a steady state without a fade effect.
// This lamp effect extends into why the 0% opacity keyframe is not in the
// midpoint: lamps take longer to turn off than they do to turn on, and the
// extra frames give it a bit of a realistic punch for when the animation is
// ramping back up to 100% opacity.
//
// Target animation timings: steady for 1.5s, fade out for 0.3s, fade in for 0.2s
// (intended to be used in a loop for 2s animation speed)
@keyframes recording-pulse {
0% {
opacity: 1;
}
35% {
opacity: 0;
}
65% {
opacity: 1;
}
}

View file

@ -189,11 +189,12 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
// See non-legacy _light for variable information
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: #ff4b55;
$voice-record-waveform-bg-color: #E3E8F0; $voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color; $voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-live-circle-color: $warning-color; $voice-record-live-circle-color: #ff4b55;
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -181,10 +181,10 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
$voice-record-waveform-bg-color: #E3E8F0; $voice-record-waveform-bg-color: #E3E8F0;
$voice-record-waveform-fg-color: $muted-fg-color; $voice-record-waveform-fg-color: $muted-fg-color;
$voice-record-live-circle-color: $warning-color; $voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -1,16 +1,15 @@
import React from "react"; import React from "react";
import ReactDom from "react-dom"; import ReactDom from "react-dom";
import Velocity from "velocity-animate";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
/** /**
* The Velociraptor contains components and animates transitions with velocity. * The NodeAnimator contains components and animates transitions.
* It will only pick up direct changes to properties ('left', currently), and so * It will only pick up direct changes to properties ('left', currently), and so
* will not work for animating positional changes where the position is implicit * will not work for animating positional changes where the position is implicit
* from DOM order. This makes it a lot simpler and lighter: if you need fully * from DOM order. This makes it a lot simpler and lighter: if you need fully
* automatic positional animation, look at react-shuffle or similar libraries. * automatic positional animation, look at react-shuffle or similar libraries.
*/ */
export default class Velociraptor extends React.Component { export default class NodeAnimator extends React.Component {
static propTypes = { static propTypes = {
// either a list of child nodes, or a single child. // either a list of child nodes, or a single child.
children: PropTypes.any, children: PropTypes.any,
@ -20,14 +19,10 @@ export default class Velociraptor extends React.Component {
// a list of state objects to apply to each child node in turn // a list of state objects to apply to each child node in turn
startStyles: PropTypes.array, startStyles: PropTypes.array,
// a list of transition options from the corresponding startStyle
enterTransitionOpts: PropTypes.array,
}; };
static defaultProps = { static defaultProps = {
startStyles: [], startStyles: [],
enterTransitionOpts: [],
}; };
constructor(props) { constructor(props) {
@ -41,6 +36,18 @@ export default class Velociraptor extends React.Component {
this._updateChildren(this.props.children); this._updateChildren(this.props.children);
} }
/**
*
* @param {HTMLElement} node element to apply styles to
* @param {object} styles a key/value pair of CSS properties
* @returns {void}
*/
_applyStyles(node, styles) {
Object.entries(styles).forEach(([property, value]) => {
node.style[property] = value;
});
}
_updateChildren(newChildren) { _updateChildren(newChildren) {
const oldChildren = this.children || {}; const oldChildren = this.children || {};
this.children = {}; this.children = {};
@ -50,17 +57,8 @@ export default class Velociraptor extends React.Component {
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]); const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
if (oldNode && oldNode.style.left !== c.props.style.left) { if (oldNode && oldNode.style.left !== c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => { this._applyStyles(oldNode, { left: c.props.style.left });
// special case visibility because it's nonsensical to animate an invisible element // console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
// so we always hidden->visible pre-transition and visible->hidden after
if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') {
oldNode.style.visibility = c.props.style.visibility;
}
});
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') {
oldNode.style.visibility = c.props.style.visibility;
} }
// clone the old element with the props (and children) of the new element // clone the old element with the props (and children) of the new element
// so prop updates are still received by the children. // so prop updates are still received by the children.
@ -94,33 +92,22 @@ export default class Velociraptor extends React.Component {
this.props.startStyles.length > 0 this.props.startStyles.length > 0
) { ) {
const startStyles = this.props.startStyles; const startStyles = this.props.startStyles;
const transitionOpts = this.props.enterTransitionOpts;
const domNode = ReactDom.findDOMNode(node); const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it // start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc. // to start with, so now we animate 1 etc.
for (var i = 1; i < startStyles.length; ++i) { for (let i = 1; i < startStyles.length; ++i) {
Velocity(domNode, startStyles[i], transitionOpts[i-1]); this._applyStyles(domNode, startStyles[i]);
/* // console.log("start:"
console.log("start:", // JSON.stringify(startStyles[i]),
JSON.stringify(transitionOpts[i-1]), // );
"->",
JSON.stringify(startStyles[i]),
);
*/
} }
// and then we animate to the resting state // and then we animate to the resting state
Velocity(domNode, restingStyle, setTimeout(() => {
transitionOpts[i-1]) this._applyStyles(domNode, restingStyle);
.then(() => { }, 0);
// once we've reached the resting state, hide the element if
// appropriate
domNode.style.visibility = restingStyle.visibility;
});
// console.log("enter:", // console.log("enter:",
// JSON.stringify(transitionOpts[i-1]),
// "->",
// JSON.stringify(restingStyle)); // JSON.stringify(restingStyle));
} }
this.nodes[k] = node; this.nodes[k] = node;
@ -128,9 +115,7 @@ export default class Velociraptor extends React.Component {
render() { render() {
return ( return (
<span> <>{ Object.values(this.children) }</>
{ Object.values(this.children) }
</span>
); );
} }
} }

View file

@ -383,6 +383,10 @@ export const Notifier = {
// don't bother notifying as user was recently active in this room // don't bother notifying as user was recently active in this room
return; return;
} }
if (SettingsStore.getValue("doNotDisturb")) {
// Don't bother the user if they didn't ask to be bothered
return;
}
if (this.isEnabled()) { if (this.isEnabled()) {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);

View file

@ -1,17 +0,0 @@
import Velocity from "velocity-animate";
// courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) {
let pow2;
let bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
// just sets pow2
}
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
}
Velocity.Easings.easeOutBounce = function(p) {
return 1 - bounce(1 - p);
};

View file

@ -659,6 +659,7 @@ export default class MessagePanel extends React.Component {
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
layout={this.props.layout} layout={this.props.layout}
enableFlair={this.props.enableFlair} enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/> />
</TileErrorBoundary> </TileErrorBoundary>
</li>, </li>,

View file

@ -74,6 +74,7 @@ interface IState {
export default class UserMenu extends React.Component<IProps, IState> { export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private themeWatcherRef: string; private themeWatcherRef: string;
private dndWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef(); private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription; private tagStoreRef: fbEmitter.EventSubscription;
@ -89,6 +90,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_spaces")) { if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
} }
// Force update is the easiest way to trigger the UI update (we don't store state for this)
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
} }
private get hasHomePage(): boolean { private get hasHomePage(): boolean {
@ -103,6 +107,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove(); this.tagStoreRef.remove();
@ -288,6 +293,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
private onDndToggle = (ev) => {
ev.stopPropagation();
const current = SettingsStore.getValue("doNotDisturb");
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
};
private renderContextMenu = (): React.ReactNode => { private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null; if (!this.state.contextMenuPosition) return null;
@ -534,6 +545,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
{/* masked image in CSS */} {/* masked image in CSS */}
</span> </span>
); );
let dnd;
if (this.state.selectedSpace) { if (this.state.selectedSpace) {
name = ( name = (
<div className="mx_UserMenu_doubleName"> <div className="mx_UserMenu_doubleName">
@ -560,6 +572,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
</div> </div>
); );
isPrototype = true; isPrototype = true;
} else if (SettingsStore.getValue("feature_dnd")) {
const isDnd = SettingsStore.getValue("doNotDisturb");
dnd = <AccessibleButton
onClick={this.onDndToggle}
className={classNames({
"mx_UserMenu_dnd": true,
"mx_UserMenu_dnd_noisy": !isDnd,
"mx_UserMenu_dnd_muted": isDnd,
})}
/>;
} }
if (this.props.isMinimized) { if (this.props.isMinimized) {
name = null; name = null;
@ -595,6 +617,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
/> />
</span> </span>
{name} {name}
{dnd}
{buttons} {buttons}
</div> </div>
</ContextMenuButton> </ContextMenuButton>

View file

@ -31,6 +31,7 @@ import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize"; import {humanizeTime} from "../../../utils/humanize";
import createRoom, { import createRoom, {
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
IInvite3PID,
} from "../../../createRoom"; } from "../../../createRoom";
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
@ -618,13 +619,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
_startDm = async () => { _startDm = async () => {
this.setState({busy: true}); this.setState({busy: true});
const client = MatrixClientPeg.get();
const targets = this._convertFilter(); const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId); const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible. // Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room; let existingRoom: Room;
if (targetIds.length === 1) { if (targetIds.length === 1) {
existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]); existingRoom = findDMForUser(client, targetIds[0]);
} else { } else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds); existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
} }
@ -646,7 +648,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// If so, enable encryption in the new room. // If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember); const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) { if (!has3PidMembers) {
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds); const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) { if (allHaveDeviceKeys) {
createRoomOptions.encryption = true; createRoomOptions.encryption = true;
@ -656,35 +657,41 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
// Check if it's a traditional DM and create the room if required. // Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM // TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>; try {
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId(); const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) { if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0]; createRoomOptions.dmUserId = targetIds[0];
createRoomPromise = createRoom(createRoomOptions); }
} else if (isSelf) {
createRoomPromise = createRoom(createRoomOptions);
} else {
// Create a boring room and try to invite the targets manually.
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
return inviteMultipleToRoom(roomId, targetIds);
}).then(result => {
if (this._shouldAbortAfterInviteError(result)) {
return true; // abort
}
});
}
// the createRoom call will show the room for us, so we don't need to worry about that. if (targetIds.length > 1) {
createRoomPromise.then(abort => { createRoomOptions.createOpts = targetIds.reduce(
if (abort === true) return; // only abort on true booleans, not roomIds or something (roomOptions, address) => {
const type = getAddressType(address);
if (type === 'email') {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true),
medium: 'email',
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === 'mx-user-id') {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
)
}
await createRoom(createRoomOptions);
this.props.onFinished(); this.props.onFinished();
}).catch(err => { } catch (err) {
console.error(err); console.error(err);
this.setState({ this.setState({
busy: false, busy: false,
errorText: _t("We couldn't create your DM."), errorText: _t("We couldn't create your DM."),
}); });
}); }
}; };
_inviteUsers = async () => { _inviteUsers = async () => {

View file

@ -260,6 +260,9 @@ export default class EventTile extends React.Component {
// whether or not to show flair at all // whether or not to show flair at all
enableFlair: PropTypes.bool, enableFlair: PropTypes.bool,
// whether or not to show read receipts
showReadReceipts: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -858,8 +861,6 @@ export default class EventTile extends React.Component {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
} }
const readAvatars = this.getReadAvatars();
let avatar; let avatar;
let sender; let sender;
let avatarSize; let avatarSize;
@ -988,6 +989,16 @@ export default class EventTile extends React.Component {
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
let msgOption;
if (this.props.showReadReceipts) {
const readAvatars = this.getReadAvatars();
msgOption = (
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
);
}
switch (this.props.tileShape) { switch (this.props.tileShape) {
case 'notif': { case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
@ -1107,9 +1118,7 @@ export default class EventTile extends React.Component {
{ reactionsRow } { reactionsRow }
{ actionBar } { actionBar }
</div> </div>
<div className="mx_EventTile_msgOption"> {msgOption}
{ readAvatars }
</div>
{ {
// The avatar goes after the event tile as it's absolutely positioned to be over the // The avatar goes after the event tile as it's absolutely positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids // event tile line, so needs to be later in the DOM so it appears on top (this avoids

View file

@ -17,22 +17,13 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import '../../../VelocityBounce';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils'; import {formatDate} from '../../../DateUtils';
import Velociraptor from "../../../Velociraptor"; import NodeAnimator from "../../../NodeAnimator";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import {toPx} from "../../../utils/units"; import {toPx} from "../../../utils/units";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
let bounce = false;
try {
if (global.localStorage) {
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
}
} catch (e) {
}
@replaceableComponent("views.rooms.ReadReceiptMarker") @replaceableComponent("views.rooms.ReadReceiptMarker")
export default class ReadReceiptMarker extends React.PureComponent { export default class ReadReceiptMarker extends React.PureComponent {
static propTypes = { static propTypes = {
@ -115,7 +106,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
// we've already done our display - nothing more to do. // we've already done our display - nothing more to do.
return; return;
} }
this._animateMarker();
}
componentDidUpdate(prevProps) {
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
const visibilityChanged = prevProps.hidden !== this.props.hidden;
if (differentLeftOffset || visibilityChanged) {
this._animateMarker();
}
}
_animateMarker() {
// treat new RRs as though they were off the top of the screen // treat new RRs as though they were off the top of the screen
let oldTop = -15; let oldTop = -15;
@ -139,42 +141,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
} }
const startStyles = []; const startStyles = [];
const enterTransitionOpts = [];
if (oldInfo && oldInfo.left) { if (oldInfo && oldInfo.left) {
// start at the old height and in the old h pos // start at the old height and in the old h pos
startStyles.push({ top: startTopOffset+"px", startStyles.push({ top: startTopOffset+"px",
left: toPx(oldInfo.left) }); left: toPx(oldInfo.left) });
const reorderTransitionOpts = {
duration: 100,
easing: 'easeOut',
};
enterTransitionOpts.push(reorderTransitionOpts);
} }
// then shift to the rightmost column, startStyles.push({ top: startTopOffset+'px', left: '0' });
// and then it will drop down to its resting position
//
// XXX: We use a small left value to trick velocity-animate into actually animating.
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
// skip applying it, thus making our read receipt at +14px instead of +0px like it
// should be. This does cause a tiny amount of drift for read receipts, however with a
// value so small it's not perceived by a user.
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
// fail to fall down or cause gaps.
startStyles.push({ top: startTopOffset+'px', left: '1px' });
enterTransitionOpts.push({
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
});
this.setState({ this.setState({
suppressDisplay: false, suppressDisplay: false,
startStyles: startStyles, startStyles: startStyles,
enterTransitionOpts: enterTransitionOpts,
}); });
} }
@ -187,7 +165,6 @@ export default class ReadReceiptMarker extends React.PureComponent {
const style = { const style = {
left: toPx(this.props.leftOffset), left: toPx(this.props.leftOffset),
top: '0px', top: '0px',
visibility: this.props.hidden ? 'hidden' : 'visible',
}; };
let title; let title;
@ -210,9 +187,8 @@ export default class ReadReceiptMarker extends React.PureComponent {
} }
return ( return (
<Velociraptor <NodeAnimator
startStyles={this.state.startStyles} startStyles={this.state.startStyles} >
enterTransitionOpts={this.state.enterTransitionOpts} >
<MemberAvatar <MemberAvatar
member={this.props.member} member={this.props.member}
fallbackUserId={this.props.fallbackUserId} fallbackUserId={this.props.fallbackUserId}
@ -223,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
onClick={this.props.onClick} onClick={this.props.onClick}
inputRef={this._avatar} inputRef={this._avatar}
/> />
</Velociraptor> </NodeAnimator>
); );
} }
} }

View file

@ -90,6 +90,12 @@ export interface IOpts {
parentSpace?: Room; parentSpace?: Room;
} }
export interface IInvite3PID {
id_server: string,
medium: 'email',
address: string,
}
/** /**
* Create a new room, and switch to it. * Create a new room, and switch to it.
* *

View file

@ -786,6 +786,7 @@
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings", "Change notification settings": "Change notification settings",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)", "Send and receive voice messages (in development)": "Send and receive voice messages (in development)",
"Render LaTeX maths in messages": "Render LaTeX maths in messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",

View file

@ -128,6 +128,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false, default: false,
controller: new ReloadOnChangeController(), controller: new ReloadOnChangeController(),
}, },
"feature_dnd": {
isFeature: true,
displayName: _td("Show options to enable 'Do not disturb' mode"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_voice_messages": { "feature_voice_messages": {
isFeature: true, isFeature: true,
displayName: _td("Send and receive voice messages (in development)"), displayName: _td("Send and receive voice messages (in development)"),
@ -226,6 +232,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false, default: false,
}, },
"doNotDisturb": {
supportedLevels: [SettingLevel.DEVICE],
default: false,
},
"mjolnirRooms": { "mjolnirRooms": {
supportedLevels: [SettingLevel.ACCOUNT], supportedLevels: [SettingLevel.ACCOUNT],
default: [], default: [],

View file

@ -22,6 +22,7 @@ import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomNotificationState } from "./RoomNotificationState"; import { RoomNotificationState } from "./RoomNotificationState";
import { SummarizedNotificationState } from "./SummarizedNotificationState"; import { SummarizedNotificationState } from "./SummarizedNotificationState";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
interface IState {} interface IState {}
@ -47,7 +48,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
// This will include highlights from the previous version of the room internally // This will include highlights from the previous version of the room internally
const globalState = new SummarizedNotificationState(); const globalState = new SummarizedNotificationState();
for (const room of this.matrixClient.getVisibleRooms()) { for (const room of this.matrixClient.getVisibleRooms()) {
globalState.add(this.getRoomState(room)); if (VisibilityProvider.instance.isRoomVisible(room)) {
globalState.add(this.getRoomState(room));
}
} }
return globalState; return globalState;
} }

View file

@ -8144,11 +8144,6 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
velocity-animate@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-2.0.6.tgz#1811ca14df7fbbef05740256f6cec0fd1b76575f"
integrity sha512-tU+/UtSo3GkIjEfk2KM4e24DvpgX0+FzfLr7XqNwm9BCvZUtbCHPq/AFutx/Mkp2bXlUS9EcX8yxu8XmzAv2Kw==
verror@1.10.0: verror@1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"