Merge branch 'develop' into travis/voice/countdown
						commit
						f955f33071
					
				|  | @ -1,7 +1,7 @@ | |||
| # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. | ||||
| 
 | ||||
| src/Markdown.js | ||||
| src/Velociraptor.js | ||||
| src/NodeAnimator.js | ||||
| src/components/structures/RoomDirectory.js | ||||
| src/components/views/rooms/MemberList.js | ||||
| src/ratelimitedfunc.js | ||||
|  |  | |||
|  | @ -102,7 +102,6 @@ | |||
|     "tar-js": "^0.3.0", | ||||
|     "text-encoding-utf-8": "^1.0.2", | ||||
|     "url": "^0.11.0", | ||||
|     "velocity-animate": "^2.0.6", | ||||
|     "what-input": "^5.2.10", | ||||
|     "zxcvbn": "^4.4.2" | ||||
|   }, | ||||
|  |  | |||
|  | @ -28,6 +28,16 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e | |||
| 
 | ||||
| :root { | ||||
|     font-size: 10px; | ||||
| 
 | ||||
|     --transition-short: .1s; | ||||
|     --transition-standard: .3s; | ||||
| } | ||||
| 
 | ||||
| @media (prefers-reduced-motion) { | ||||
|     :root { | ||||
|         --transition-short: 0; | ||||
|         --transition-standard: 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| html { | ||||
|  |  | |||
|  | @ -21,6 +21,5 @@ limitations under the License. | |||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: flex-end; | ||||
|         overflow-y: hidden; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -117,6 +117,32 @@ limitations under the License. | |||
|         .mx_UserMenu_headerButtons { | ||||
|             // 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 { | ||||
|  |  | |||
|  | @ -283,6 +283,10 @@ $left-gutter: 64px; | |||
|     display: inline-block; | ||||
|     height: $font-14px; | ||||
|     width: $font-14px; | ||||
| 
 | ||||
|     transition: | ||||
|         left var(--transition-short) ease-out, | ||||
|         top var(--transition-standard) ease-out; | ||||
| } | ||||
| 
 | ||||
| .mx_EventTile_readAvatarRemainder { | ||||
|  |  | |||
|  | @ -53,7 +53,8 @@ limitations under the License. | |||
|     font-size: $font-14px; | ||||
| 
 | ||||
|     &::before { | ||||
|         // TODO: @@ TravisR: Animate | ||||
|         animation: recording-pulse 2s infinite; | ||||
| 
 | ||||
|         content: ''; | ||||
|         background-color: $voice-record-live-circle-color; | ||||
|         width: 10px; | ||||
|  | @ -74,3 +75,26 @@ limitations under the License. | |||
|         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; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -189,11 +189,12 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) | |||
| 
 | ||||
| $groupFilterPanel-divider-color: $roomlist-header-color; | ||||
| 
 | ||||
| // See non-legacy _light for variable information | ||||
| $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-fg-color: $muted-fg-color; | ||||
| $voice-record-live-circle-color: $warning-color; | ||||
| $voice-record-live-circle-color: #ff4b55; | ||||
| 
 | ||||
| $roomtile-preview-color: #9e9e9e; | ||||
| $roomtile-default-badge-bg-color: #61708b; | ||||
|  |  | |||
|  | @ -181,10 +181,10 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) | |||
| $groupFilterPanel-divider-color: $roomlist-header-color; | ||||
| 
 | ||||
| $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-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-default-badge-bg-color: #61708b; | ||||
|  |  | |||
|  | @ -1,16 +1,15 @@ | |||
| import React from "react"; | ||||
| import ReactDom from "react-dom"; | ||||
| import Velocity from "velocity-animate"; | ||||
| 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 | ||||
|  * 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 | ||||
|  * 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 = { | ||||
|         // either a list of child nodes, or a single child.
 | ||||
|         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
 | ||||
|         startStyles: PropTypes.array, | ||||
| 
 | ||||
|         // a list of transition options from the corresponding startStyle
 | ||||
|         enterTransitionOpts: PropTypes.array, | ||||
|     }; | ||||
| 
 | ||||
|     static defaultProps = { | ||||
|         startStyles: [], | ||||
|         enterTransitionOpts: [], | ||||
|     }; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|  | @ -41,6 +36,18 @@ export default class Velociraptor extends React.Component { | |||
|         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) { | ||||
|         const oldChildren = this.children || {}; | ||||
|         this.children = {}; | ||||
|  | @ -50,17 +57,8 @@ export default class Velociraptor extends React.Component { | |||
|                 const oldNode = ReactDom.findDOMNode(this.nodes[old.key]); | ||||
| 
 | ||||
|                 if (oldNode && oldNode.style.left !== c.props.style.left) { | ||||
|                     Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => { | ||||
|                         // special case visibility because it's nonsensical to animate an invisible element
 | ||||
|                         // 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; | ||||
|                     this._applyStyles(oldNode, { left: c.props.style.left }); | ||||
|                     // console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
 | ||||
|                 } | ||||
|                 // clone the old element with the props (and children) of the new element
 | ||||
|                 // 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 | ||||
|         ) { | ||||
|             const startStyles = this.props.startStyles; | ||||
|             const transitionOpts = this.props.enterTransitionOpts; | ||||
|             const domNode = ReactDom.findDOMNode(node); | ||||
|             // start from startStyle 1: 0 is the one we gave it
 | ||||
|             // to start with, so now we animate 1 etc.
 | ||||
|             for (var i = 1; i < startStyles.length; ++i) { | ||||
|                 Velocity(domNode, startStyles[i], transitionOpts[i-1]); | ||||
|                 /* | ||||
|                 console.log("start:", | ||||
|                             JSON.stringify(transitionOpts[i-1]), | ||||
|                             "->", | ||||
|                             JSON.stringify(startStyles[i]), | ||||
|                             ); | ||||
|                 */ | ||||
|             for (let i = 1; i < startStyles.length; ++i) { | ||||
|                 this._applyStyles(domNode, startStyles[i]); | ||||
|                 // console.log("start:"
 | ||||
|                 //             JSON.stringify(startStyles[i]),
 | ||||
|                 //             );
 | ||||
|             } | ||||
| 
 | ||||
|             // and then we animate to the resting state
 | ||||
|             Velocity(domNode, restingStyle, | ||||
|                 transitionOpts[i-1]) | ||||
|                 .then(() => { | ||||
|                     // once we've reached the resting state, hide the element if
 | ||||
|                     // appropriate
 | ||||
|                     domNode.style.visibility = restingStyle.visibility; | ||||
|                 }); | ||||
|             setTimeout(() => { | ||||
|                 this._applyStyles(domNode, restingStyle); | ||||
|             }, 0); | ||||
| 
 | ||||
|             // console.log("enter:",
 | ||||
|             //             JSON.stringify(transitionOpts[i-1]),
 | ||||
|             //             "->",
 | ||||
|             //             JSON.stringify(restingStyle));
 | ||||
|         } | ||||
|         this.nodes[k] = node; | ||||
|  | @ -128,9 +115,7 @@ export default class Velociraptor extends React.Component { | |||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <span> | ||||
|                 { Object.values(this.children) } | ||||
|             </span> | ||||
|             <>{ Object.values(this.children) }</> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -383,6 +383,10 @@ export const Notifier = { | |||
|                 // don't bother notifying as user was recently active in this room
 | ||||
|                 return; | ||||
|             } | ||||
|             if (SettingsStore.getValue("doNotDisturb")) { | ||||
|                 // Don't bother the user if they didn't ask to be bothered
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (this.isEnabled()) { | ||||
|                 this._displayPopupNotification(ev, room); | ||||
|  |  | |||
|  | @ -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); | ||||
| }; | ||||
|  | @ -659,6 +659,7 @@ export default class MessagePanel extends React.Component { | |||
|                         showReactions={this.props.showReactions} | ||||
|                         layout={this.props.layout} | ||||
|                         enableFlair={this.props.enableFlair} | ||||
|                         showReadReceipts={this.props.showReadReceipts} | ||||
|                     /> | ||||
|                 </TileErrorBoundary> | ||||
|             </li>, | ||||
|  |  | |||
|  | @ -74,6 +74,7 @@ interface IState { | |||
| export default class UserMenu extends React.Component<IProps, IState> { | ||||
|     private dispatcherRef: string; | ||||
|     private themeWatcherRef: string; | ||||
|     private dndWatcherRef: string; | ||||
|     private buttonRef: React.RefObject<HTMLButtonElement> = createRef(); | ||||
|     private tagStoreRef: fbEmitter.EventSubscription; | ||||
| 
 | ||||
|  | @ -89,6 +90,9 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|         if (SettingsStore.getValue("feature_spaces")) { | ||||
|             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 { | ||||
|  | @ -103,6 +107,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
| 
 | ||||
|     public componentWillUnmount() { | ||||
|         if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); | ||||
|         if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef); | ||||
|         if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); | ||||
|         OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); | ||||
|         this.tagStoreRef.remove(); | ||||
|  | @ -288,6 +293,12 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|         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 => { | ||||
|         if (!this.state.contextMenuPosition) return null; | ||||
| 
 | ||||
|  | @ -534,6 +545,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|                 {/* masked image in CSS */} | ||||
|             </span> | ||||
|         ); | ||||
|         let dnd; | ||||
|         if (this.state.selectedSpace) { | ||||
|             name = ( | ||||
|                 <div className="mx_UserMenu_doubleName"> | ||||
|  | @ -560,6 +572,16 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|                 </div> | ||||
|             ); | ||||
|             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) { | ||||
|             name = null; | ||||
|  | @ -595,6 +617,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|                             /> | ||||
|                         </span> | ||||
|                         {name} | ||||
|                         {dnd} | ||||
|                         {buttons} | ||||
|                     </div> | ||||
|                 </ContextMenuButton> | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ import Modal from "../../../Modal"; | |||
| import {humanizeTime} from "../../../utils/humanize"; | ||||
| import createRoom, { | ||||
|     canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, | ||||
|     IInvite3PID, | ||||
| } from "../../../createRoom"; | ||||
| import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; | ||||
| import {Key} from "../../../Keyboard"; | ||||
|  | @ -618,13 +619,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps | |||
| 
 | ||||
|     _startDm = async () => { | ||||
|         this.setState({busy: true}); | ||||
|         const client = MatrixClientPeg.get(); | ||||
|         const targets = this._convertFilter(); | ||||
|         const targetIds = targets.map(t => t.userId); | ||||
| 
 | ||||
|         // Check if there is already a DM with these people and reuse it if possible.
 | ||||
|         let existingRoom: Room; | ||||
|         if (targetIds.length === 1) { | ||||
|             existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]); | ||||
|             existingRoom = findDMForUser(client, targetIds[0]); | ||||
|         } else { | ||||
|             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.
 | ||||
|             const has3PidMembers = targets.some(t => t instanceof ThreepidMember); | ||||
|             if (!has3PidMembers) { | ||||
|                 const client = MatrixClientPeg.get(); | ||||
|                 const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds); | ||||
|                 if (allHaveDeviceKeys) { | ||||
|                     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.
 | ||||
|         // TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
 | ||||
|         let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>; | ||||
|         const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId(); | ||||
|         if (targetIds.length === 1 && !isSelf) { | ||||
|             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
 | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|         try { | ||||
|             const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId(); | ||||
|             if (targetIds.length === 1 && !isSelf) { | ||||
|                 createRoomOptions.dmUserId = targetIds[0]; | ||||
|             } | ||||
| 
 | ||||
|         // the createRoom call will show the room for us, so we don't need to worry about that.
 | ||||
|         createRoomPromise.then(abort => { | ||||
|             if (abort === true) return; // only abort on true booleans, not roomIds or something
 | ||||
|             if (targetIds.length > 1) { | ||||
|                 createRoomOptions.createOpts = targetIds.reduce( | ||||
|                     (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(); | ||||
|         }).catch(err => { | ||||
|         } catch (err) { | ||||
|             console.error(err); | ||||
|             this.setState({ | ||||
|                 busy: false, | ||||
|                 errorText: _t("We couldn't create your DM."), | ||||
|             }); | ||||
|         }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     _inviteUsers = async () => { | ||||
|  |  | |||
|  | @ -260,6 +260,9 @@ export default class EventTile extends React.Component { | |||
| 
 | ||||
|         // whether or not to show flair at all
 | ||||
|         enableFlair: PropTypes.bool, | ||||
| 
 | ||||
|         // whether or not to show read receipts
 | ||||
|         showReadReceipts: PropTypes.bool, | ||||
|     }; | ||||
| 
 | ||||
|     static defaultProps = { | ||||
|  | @ -858,8 +861,6 @@ export default class EventTile extends React.Component { | |||
|             permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); | ||||
|         } | ||||
| 
 | ||||
|         const readAvatars = this.getReadAvatars(); | ||||
| 
 | ||||
|         let avatar; | ||||
|         let sender; | ||||
|         let avatarSize; | ||||
|  | @ -988,6 +989,16 @@ export default class EventTile extends React.Component { | |||
|         const groupPadlock = !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) { | ||||
|             case 'notif': { | ||||
|                 const room = this.context.getRoom(this.props.mxEvent.getRoomId()); | ||||
|  | @ -1107,9 +1118,7 @@ export default class EventTile extends React.Component { | |||
|                             { reactionsRow } | ||||
|                             { actionBar } | ||||
|                         </div> | ||||
|                         <div className="mx_EventTile_msgOption"> | ||||
|                             { readAvatars } | ||||
|                         </div> | ||||
|                         {msgOption} | ||||
|                         { | ||||
|                             // 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
 | ||||
|  |  | |||
|  | @ -17,22 +17,13 @@ limitations under the License. | |||
| 
 | ||||
| import React, {createRef} from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import '../../../VelocityBounce'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import {formatDate} from '../../../DateUtils'; | ||||
| import Velociraptor from "../../../Velociraptor"; | ||||
| import NodeAnimator from "../../../NodeAnimator"; | ||||
| import * as sdk from "../../../index"; | ||||
| import {toPx} from "../../../utils/units"; | ||||
| 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") | ||||
| export default class ReadReceiptMarker extends React.PureComponent { | ||||
|     static propTypes = { | ||||
|  | @ -115,7 +106,18 @@ export default class ReadReceiptMarker extends React.PureComponent { | |||
|             // we've already done our display - nothing more to do.
 | ||||
|             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
 | ||||
|         let oldTop = -15; | ||||
| 
 | ||||
|  | @ -139,42 +141,18 @@ export default class ReadReceiptMarker extends React.PureComponent { | |||
|         } | ||||
| 
 | ||||
|         const startStyles = []; | ||||
|         const enterTransitionOpts = []; | ||||
| 
 | ||||
|         if (oldInfo && oldInfo.left) { | ||||
|             // start at the old height and in the old h pos
 | ||||
| 
 | ||||
|             startStyles.push({ top: startTopOffset+"px", | ||||
|                                left: toPx(oldInfo.left) }); | ||||
| 
 | ||||
|             const reorderTransitionOpts = { | ||||
|                 duration: 100, | ||||
|                 easing: 'easeOut', | ||||
|             }; | ||||
| 
 | ||||
|             enterTransitionOpts.push(reorderTransitionOpts); | ||||
|         } | ||||
| 
 | ||||
|         // then shift to the rightmost column,
 | ||||
|         // 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', | ||||
|         }); | ||||
|         startStyles.push({ top: startTopOffset+'px', left: '0' }); | ||||
| 
 | ||||
|         this.setState({ | ||||
|             suppressDisplay: false, | ||||
|             startStyles: startStyles, | ||||
|             enterTransitionOpts: enterTransitionOpts, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | @ -187,7 +165,6 @@ export default class ReadReceiptMarker extends React.PureComponent { | |||
|         const style = { | ||||
|             left: toPx(this.props.leftOffset), | ||||
|             top: '0px', | ||||
|             visibility: this.props.hidden ? 'hidden' : 'visible', | ||||
|         }; | ||||
| 
 | ||||
|         let title; | ||||
|  | @ -210,9 +187,8 @@ export default class ReadReceiptMarker extends React.PureComponent { | |||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <Velociraptor | ||||
|                     startStyles={this.state.startStyles} | ||||
|                     enterTransitionOpts={this.state.enterTransitionOpts} > | ||||
|             <NodeAnimator | ||||
|                     startStyles={this.state.startStyles} > | ||||
|                 <MemberAvatar | ||||
|                     member={this.props.member} | ||||
|                     fallbackUserId={this.props.fallbackUserId} | ||||
|  | @ -223,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent { | |||
|                     onClick={this.props.onClick} | ||||
|                     inputRef={this._avatar} | ||||
|                 /> | ||||
|             </Velociraptor> | ||||
|             </NodeAnimator> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -90,6 +90,12 @@ export interface IOpts { | |||
|     parentSpace?: Room; | ||||
| } | ||||
| 
 | ||||
| export interface IInvite3PID { | ||||
|     id_server: string, | ||||
|     medium: 'email', | ||||
|     address: string, | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Create a new room, and switch to it. | ||||
|  * | ||||
|  |  | |||
|  | @ -786,6 +786,7 @@ | |||
|     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", | ||||
|     "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.", | ||||
|     "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)", | ||||
|     "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.", | ||||
|  |  | |||
|  | @ -128,6 +128,12 @@ export const SETTINGS: {[setting: string]: ISetting} = { | |||
|         default: false, | ||||
|         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": { | ||||
|         isFeature: true, | ||||
|         displayName: _td("Send and receive voice messages (in development)"), | ||||
|  | @ -226,6 +232,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { | |||
|         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, | ||||
|         default: false, | ||||
|     }, | ||||
|     "doNotDisturb": { | ||||
|         supportedLevels: [SettingLevel.DEVICE], | ||||
|         default: false, | ||||
|     }, | ||||
|     "mjolnirRooms": { | ||||
|         supportedLevels: [SettingLevel.ACCOUNT], | ||||
|         default: [], | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import { FetchRoomFn, ListNotificationState } from "./ListNotificationState"; | |||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import { RoomNotificationState } from "./RoomNotificationState"; | ||||
| import { SummarizedNotificationState } from "./SummarizedNotificationState"; | ||||
| import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; | ||||
| 
 | ||||
| interface IState {} | ||||
| 
 | ||||
|  | @ -47,7 +48,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> { | |||
|         // This will include highlights from the previous version of the room internally
 | ||||
|         const globalState = new SummarizedNotificationState(); | ||||
|         for (const room of this.matrixClient.getVisibleRooms()) { | ||||
|             globalState.add(this.getRoomState(room)); | ||||
|             if (VisibilityProvider.instance.isRoomVisible(room)) { | ||||
|                 globalState.add(this.getRoomState(room)); | ||||
|             } | ||||
|         } | ||||
|         return globalState; | ||||
|     } | ||||
|  |  | |||
|  | @ -8144,11 +8144,6 @@ validate-npm-package-license@^3.0.1: | |||
|     spdx-correct "^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: | ||||
|   version "1.10.0" | ||||
|   resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston