Merge remote-tracking branch 'origin/develop' into dbkr/fix_roomlist_prompt
This commit is contained in:
commit
4f53e6cddd
17 changed files with 218 additions and 154 deletions
|
@ -23,11 +23,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile[data-layout=bubble] {
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: var(--gutterSize);
|
margin-top: var(--gutterSize);
|
||||||
margin-left: 50px;
|
margin-left: 49px;
|
||||||
margin-right: 100px;
|
margin-right: 100px;
|
||||||
|
font-size: $font-14px;
|
||||||
|
|
||||||
&.mx_EventTile_continuation {
|
&.mx_EventTile_continuation {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -77,10 +77,11 @@ limitations under the License.
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SenderProfile {
|
> .mx_SenderProfile {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
|
font-size: $font-15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-self=false] {
|
&[data-self=false] {
|
||||||
|
@ -113,8 +114,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ReplyTile .mx_SenderProfile {
|
.mx_ReplyTile .mx_SenderProfile {
|
||||||
display: block;
|
display: block;
|
||||||
top: unset;
|
|
||||||
left: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow {
|
||||||
|
@ -287,6 +286,8 @@ limitations under the License.
|
||||||
.mx_EventTile_line,
|
.mx_EventTile_line,
|
||||||
.mx_EventTile_info {
|
.mx_EventTile_info {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
// Preserve alignment with left edge of text in bubbles
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_e2eIcon {
|
.mx_EventTile_e2eIcon {
|
||||||
|
@ -294,9 +295,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line > a {
|
.mx_EventTile_line > a {
|
||||||
|
// Align timestamps with those of normal bubble tiles
|
||||||
right: auto;
|
right: auto;
|
||||||
top: -15px;
|
top: -11px;
|
||||||
left: -68px;
|
left: -95px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,11 +328,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
margin: 0 5px;
|
margin: 0;
|
||||||
> a {
|
> a {
|
||||||
left: auto;
|
// Align timestamps with those of normal bubble tiles
|
||||||
right: 0;
|
left: -76px;
|
||||||
transform: translateX(calc(100% + 5px));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +341,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
|
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
|
||||||
padding: 0 34px;
|
// Align with left edge of bubble tiles
|
||||||
|
padding: 0 49px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* events that do not require bubble layout */
|
/* events that do not require bubble layout */
|
||||||
|
|
|
@ -172,14 +172,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the general case, we leave height of headers alone even if sticky, so
|
// In the general case, we reserve space for each sublist header to prevent
|
||||||
// that the sublists below them do not jump. However, that leaves a gap
|
// scroll jumps when they become sticky. However, that leaves a gap when
|
||||||
// when scrolled to the top above the first sublist (whose header can only
|
// scrolled to the top above the first sublist (whose header can only ever
|
||||||
// ever stick to top), so we force height to 0 for only that first header.
|
// stick to top), so we make sure to exclude the first visible sublist.
|
||||||
// See also https://github.com/vector-im/element-web/issues/14429.
|
&:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_headerContainer {
|
||||||
&:first-child .mx_RoomSublist_headerContainer {
|
height: 24px;
|
||||||
height: 0;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSublist_resizeBox {
|
.mx_RoomSublist_resizeBox {
|
||||||
|
|
|
@ -574,11 +574,12 @@ async function doSetLoggedIn(
|
||||||
await abortLogin();
|
await abortLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
|
||||||
|
|
||||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||||
|
|
||||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { SettingLevel } from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
|
||||||
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
||||||
export enum MediaDeviceKindEnum {
|
export enum MediaDeviceKindEnum {
|
||||||
|
@ -74,8 +74,8 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAudioOutput(deviceId: string): void {
|
public setAudioOutput(deviceId: string): void {
|
||||||
|
@ -90,7 +90,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public setAudioInput(deviceId: string): void {
|
public setAudioInput(deviceId: string): void {
|
||||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallAudioInput(deviceId);
|
MatrixClientPeg.get().getMediaHandler().setAudioInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,7 +100,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public setVideoInput(deviceId: string): void {
|
public setVideoInput(deviceId: string): void {
|
||||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallVideoInput(deviceId);
|
MatrixClientPeg.get().getMediaHandler().setVideoInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
||||||
|
|
|
@ -18,6 +18,8 @@ import posthog, { PostHog } from 'posthog-js';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
/* Posthog analytics tracking.
|
/* Posthog analytics tracking.
|
||||||
*
|
*
|
||||||
|
@ -141,6 +143,7 @@ export class PosthogAnalytics {
|
||||||
private enabled = false;
|
private enabled = false;
|
||||||
private static _instance = null;
|
private static _instance = null;
|
||||||
private platformSuperProperties = {};
|
private platformSuperProperties = {};
|
||||||
|
private static ANALYTICS_ID_EVENT_TYPE = "im.vector.web.analytics_id";
|
||||||
|
|
||||||
public static get instance(): PosthogAnalytics {
|
public static get instance(): PosthogAnalytics {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
|
@ -274,9 +277,32 @@ export class PosthogAnalytics {
|
||||||
this.anonymity = anonymity;
|
this.anonymity = anonymity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async identifyUser(userId: string): Promise<void> {
|
private static getRandomAnalyticsId(): string {
|
||||||
|
return [...crypto.getRandomValues(new Uint8Array(16))].map((c) => c.toString(16)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async identifyUser(client: MatrixClient, analyticsIdGenerator: () => string): Promise<void> {
|
||||||
if (this.anonymity == Anonymity.Pseudonymous) {
|
if (this.anonymity == Anonymity.Pseudonymous) {
|
||||||
this.posthog.identify(await hashHex(userId));
|
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
|
||||||
|
// different devices to send the same ID.
|
||||||
|
try {
|
||||||
|
const accountData = await client.getAccountDataFromServer(PosthogAnalytics.ANALYTICS_ID_EVENT_TYPE);
|
||||||
|
let analyticsID = accountData?.id;
|
||||||
|
if (!analyticsID) {
|
||||||
|
// Couldn't retrieve an analytics ID from user settings, so create one and set it on the server.
|
||||||
|
// Note there's a race condition here - if two devices do these steps at the same time, last write
|
||||||
|
// wins, and the first writer will send tracking with an ID that doesn't match the one on the server
|
||||||
|
// until the next time account data is refreshed and this function is called (most likely on next
|
||||||
|
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
|
||||||
|
analyticsID = analyticsIdGenerator();
|
||||||
|
await client.setAccountData("im.vector.web.analytics_id", { id: analyticsID });
|
||||||
|
}
|
||||||
|
this.posthog.identify(analyticsID);
|
||||||
|
} catch (e) {
|
||||||
|
// The above could fail due to network requests, but not essential to starting the application,
|
||||||
|
// so swallow it.
|
||||||
|
console.log("Unable to identify user for tracking" + e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +375,7 @@ export class PosthogAnalytics {
|
||||||
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
||||||
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
||||||
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
||||||
await this.identifyUser(userId);
|
await this.identifyUser(MatrixClientPeg.get(), PosthogAnalytics.getRandomAnalyticsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,6 @@ export default class Resend {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_failed',
|
|
||||||
event: event,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1897,15 +1897,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (!cli) {
|
if (!cli) return;
|
||||||
dis.dispatch({ action: 'message_send_failed' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
|
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
|
||||||
dis.dispatch({ action: 'message_sent' });
|
dis.dispatch({ action: 'message_sent' });
|
||||||
}, (err) => {
|
|
||||||
dis.dispatch({ action: 'message_send_failed' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,43 +15,48 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
member: RoomMember;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizeMethod?: ResizeMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hasStatus: boolean;
|
||||||
|
menuDisplayed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
||||||
export default class MemberStatusMessageAvatar extends React.Component {
|
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
member: PropTypes.object.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
};
|
};
|
||||||
|
private button = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._button = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||||
}
|
}
|
||||||
|
@ -62,44 +67,44 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasStatus() {
|
private get hasStatus(): boolean {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!user._unstable_statusMessage;
|
return !!user.unstable_statusMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
openMenu = () => {
|
private openMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
closeMenu = () => {
|
private closeMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: false });
|
this.setState({ menuDisplayed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const avatar = <MemberAvatar
|
const avatar = <MemberAvatar
|
||||||
member={this.props.member}
|
member={this.props.member}
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
|
@ -118,7 +123,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
const elementRect = this._button.current.getBoundingClientRect();
|
const elementRect = this.button.current.getBoundingClientRect();
|
||||||
|
|
||||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||||
const chevronMargin = 1; // Add some spacing away from target
|
const chevronMargin = 1; // Add some spacing away from target
|
||||||
|
@ -126,13 +131,13 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
||||||
chevronFace="bottom"
|
chevronFace={ChevronFace.Bottom}
|
||||||
left={elementRect.left + window.pageXOffset}
|
left={elementRect.left + window.pageXOffset}
|
||||||
top={elementRect.top + window.pageYOffset - chevronMargin}
|
top={elementRect.top + window.pageYOffset - chevronMargin}
|
||||||
menuWidth={226}
|
menuWidth={226}
|
||||||
onFinished={this.closeMenu}
|
onFinished={this.closeMenu}
|
||||||
>
|
>
|
||||||
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
|
<StatusMessageContextMenu user={this.props.member.user} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className={classes}
|
className={classes}
|
||||||
inputRef={this._button}
|
inputRef={this.button}
|
||||||
onClick={this.openMenu}
|
onClick={this.openMenu}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
label={_t("User Status")}
|
label={_t("User Status")}
|
|
@ -15,45 +15,41 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
|
element: React.ReactNode;
|
||||||
|
// Function to be called when the parent window is resized
|
||||||
|
// This can be used to reposition or close the menu on resize and
|
||||||
|
// ensure that it is not displayed in a stale position.
|
||||||
|
onResize?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* This component can be used to display generic HTML content in a contextual
|
* This component can be used to display generic HTML content in a contextual
|
||||||
* menu.
|
* menu.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
||||||
export default class GenericElementContextMenu extends React.Component {
|
export default class GenericElementContextMenu extends React.Component<IProps> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
element: PropTypes.element.isRequired,
|
|
||||||
// Function to be called when the parent window is resized
|
|
||||||
// This can be used to reposition or close the menu on resize and
|
|
||||||
// ensure that it is not displayed in a stale position.
|
|
||||||
onResize: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
this.resize = this.resize.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.resize = this.resize.bind(this);
|
|
||||||
window.addEventListener("resize", this.resize);
|
window.addEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
private resize = (): void => {
|
||||||
if (this.props.onResize) {
|
if (this.props.onResize) {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.element }</div>;
|
return <div>{ this.props.element }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,16 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
interface IProps {
|
||||||
export default class GenericTextContextMenu extends React.Component {
|
message: string;
|
||||||
static propTypes = {
|
}
|
||||||
message: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
||||||
|
export default class GenericTextContextMenu extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.message }</div>;
|
return <div>{ this.props.message }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,53 +14,59 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import * as sdk from '../../../index';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// js-sdk User object. Not required because it might not exist.
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
message: string;
|
||||||
|
waiting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
||||||
export default class StatusMessageContextMenu extends React.Component {
|
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
// js-sdk User object. Not required because it might not exist.
|
|
||||||
user: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
waiting: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get comittedStatusMessage() {
|
get comittedStatusMessage(): string {
|
||||||
return this.props.user ? this.props.user._unstable_statusMessage : "";
|
return this.props.user ? this.props.user.unstable_statusMessage : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
@ -68,14 +74,14 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClearClick = (e) => {
|
private onClearClick = (): void=> {
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
||||||
this.setState({
|
this.setState({
|
||||||
waiting: true,
|
waiting: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSubmit = (e) => {
|
private onSubmit = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -83,27 +89,25 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onStatusChange = (e) => {
|
private onStatusChange = (e: ChangeEvent): void => {
|
||||||
// The input field's value was changed.
|
// The input field's value was changed.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: e.target.value,
|
message: (e.target as HTMLInputElement).value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
|
|
||||||
let actionButton;
|
let actionButton;
|
||||||
if (this.comittedStatusMessage) {
|
if (this.comittedStatusMessage) {
|
||||||
if (this.state.message === this.comittedStatusMessage) {
|
if (this.state.message === this.comittedStatusMessage) {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
||||||
onClick={this._onClearClick}
|
onClick={this.onClearClick}
|
||||||
>
|
>
|
||||||
<span>{ _t("Clear status") }</span>
|
<span>{ _t("Clear status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Update status") }</span>
|
<span>{ _t("Update status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -112,7 +116,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
actionButton = <AccessibleButton
|
actionButton = <AccessibleButton
|
||||||
className="mx_StatusMessageContextMenu_submit"
|
className="mx_StatusMessageContextMenu_submit"
|
||||||
disabled={!this.state.message}
|
disabled={!this.state.message}
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Set status") }</span>
|
<span>{ _t("Set status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -120,13 +124,13 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
|
|
||||||
let spinner = null;
|
let spinner = null;
|
||||||
if (this.state.waiting) {
|
if (this.state.waiting) {
|
||||||
spinner = <Spinner w="24" h="24" />;
|
spinner = <Spinner w={24} h={24} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = <form
|
const form = <form
|
||||||
className="mx_StatusMessageContextMenu_form"
|
className="mx_StatusMessageContextMenu_form"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onSubmit={this._onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -134,9 +138,9 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
key="message"
|
key="message"
|
||||||
placeholder={_t("Set a new status...")}
|
placeholder={_t("Set a new status...")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
maxLength="60"
|
maxLength={60}
|
||||||
value={this.state.message}
|
value={this.state.message}
|
||||||
onChange={this._onStatusChange}
|
onChange={this.onStatusChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_StatusMessageContextMenu_actionContainer">
|
<div className="mx_StatusMessageContextMenu_actionContainer">
|
||||||
{ actionButton }
|
{ actionButton }
|
|
@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// matches emoticons which follow the start of a line or whitespace
|
// matches emoticons which follow the start of a line or whitespace
|
||||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||||
|
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private replaceEmoticon = (caretPosition: DocumentPosition): number => {
|
public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
|
||||||
const { model } = this.props;
|
const { model } = this.props;
|
||||||
const range = model.startRange(caretPosition);
|
const range = model.startRange(caretPosition);
|
||||||
// expand range max 8 characters backwards from caretPosition,
|
// expand range max 8 characters backwards from caretPosition,
|
||||||
|
@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
range.expandBackwardsWhile((index, offset) => {
|
range.expandBackwardsWhile((index, offset) => {
|
||||||
const part = model.parts[index];
|
const part = model.parts[index];
|
||||||
n -= 1;
|
n -= 1;
|
||||||
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
|
return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
|
||||||
});
|
});
|
||||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
const emoticonMatch = regex.exec(range.text);
|
||||||
if (emoticonMatch) {
|
if (emoticonMatch) {
|
||||||
const query = emoticonMatch[1].replace("-", "");
|
const query = emoticonMatch[1].replace("-", "");
|
||||||
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
||||||
|
@ -180,18 +181,23 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const { partCreator } = model;
|
const { partCreator } = model;
|
||||||
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
|
const moveStart = emoticonMatch[0][0] === " " ? 1 : 0;
|
||||||
|
const moveEnd = emoticonMatch[0].length - emoticonMatch.length - moveStart;
|
||||||
|
|
||||||
// we need the range to only comprise of the emoticon
|
// we need the range to only comprise of the emoticon
|
||||||
// because we'll replace the whole range with an emoji,
|
// because we'll replace the whole range with an emoji,
|
||||||
// so move the start forward to the start of the emoticon.
|
// so move the start forward to the start of the emoticon.
|
||||||
// Take + 1 because index is reported without the possible preceding space.
|
// Take + 1 because index is reported without the possible preceding space.
|
||||||
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
|
range.moveStartForwards(emoticonMatch.index + moveStart);
|
||||||
|
// and move end backwards so that we don't replace the trailing space/newline
|
||||||
|
range.moveEndBackwards(moveEnd);
|
||||||
|
|
||||||
// this returns the amount of added/removed characters during the replace
|
// this returns the amount of added/removed characters during the replace
|
||||||
// so the caret position can be adjusted.
|
// so the caret position can be adjusted.
|
||||||
return range.replace([partCreator.plain(data.unicode + " ")]);
|
return range.replace([partCreator.plain(data.unicode)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||||
renderModel(this.editorRef.current, this.props.model);
|
renderModel(this.editorRef.current, this.props.model);
|
||||||
|
@ -607,8 +613,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private configureEmoticonAutoReplace = (): void => {
|
private configureEmoticonAutoReplace = (): void => {
|
||||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
this.props.model.setTransformCallback(this.transform);
|
||||||
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private configureShouldShowPillAvatar = (): void => {
|
private configureShouldShowPillAvatar = (): void => {
|
||||||
|
@ -621,6 +626,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.setState({ surroundWith });
|
this.setState({ surroundWith });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private transform = (documentPosition: DocumentPosition): void => {
|
||||||
|
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||||
|
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||||
|
|
|
@ -57,7 +57,7 @@ let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
const NARROW_MODE_BREAKPOINT = 500;
|
||||||
|
|
||||||
interface IComposerAvatarProps {
|
interface IComposerAvatarProps {
|
||||||
me: object;
|
me: RoomMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||||
|
|
|
@ -31,8 +31,8 @@ import {
|
||||||
textSerialize,
|
textSerialize,
|
||||||
unescapeMessage,
|
unescapeMessage,
|
||||||
} from '../../../editor/serialize';
|
} from '../../../editor/serialize';
|
||||||
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import SendHistoryManager from "../../../SendHistoryManager";
|
import SendHistoryManager from "../../../SendHistoryManager";
|
||||||
|
@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(): Promise<void> {
|
public async sendMessage(): Promise<void> {
|
||||||
if (this.model.isEmpty) {
|
const model = this.model;
|
||||||
|
|
||||||
|
if (model.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}
|
||||||
|
|
||||||
const replyToEvent = this.props.replyToEvent;
|
const replyToEvent = this.props.replyToEvent;
|
||||||
let shouldSend = true;
|
let shouldSend = true;
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
if (!containsEmote(model) && this.isSlashCommand()) {
|
||||||
const [cmd, args, commandText] = this.getSlashCommand();
|
const [cmd, args, commandText] = this.getSlashCommand();
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (cmd.category === CommandCategories.messages) {
|
if (cmd.category === CommandCategories.messages) {
|
||||||
|
@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQuickReaction(this.model)) {
|
if (isQuickReaction(model)) {
|
||||||
shouldSend = false;
|
shouldSend = false;
|
||||||
this.sendQuickReaction();
|
this.sendQuickReaction();
|
||||||
}
|
}
|
||||||
|
@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
const { roomId } = this.props.room;
|
const { roomId } = this.props.room;
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
this.model,
|
model,
|
||||||
replyToEvent,
|
replyToEvent,
|
||||||
this.props.replyInThread,
|
this.props.replyInThread,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
|
@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendHistoryManager.save(this.model, replyToEvent);
|
this.sendHistoryManager.save(model, replyToEvent);
|
||||||
// clear composer
|
// clear composer
|
||||||
this.model.reset([]);
|
model.reset([]);
|
||||||
this.editorRef.current?.clearUndoHistory();
|
this.editorRef.current?.clearUndoHistory();
|
||||||
this.editorRef.current?.focus();
|
this.editorRef.current?.focus();
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
|
|
|
@ -277,9 +277,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
if (this.state.screensharing) {
|
if (this.state.screensharing) {
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(false);
|
isScreensharing = await this.props.call.setScreensharingEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
if (window.electron?.getDesktopCapturerSources) {
|
||||||
const [source] = await finished;
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
const [source] = await finished;
|
||||||
|
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
||||||
|
} else {
|
||||||
|
isScreensharing = await this.props.call.setScreensharingEnabled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -32,13 +32,20 @@ export default class Range {
|
||||||
this._end = bIsLarger ? positionB : positionA;
|
this._end = bIsLarger ? positionB : positionA;
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveStart(delta: number): void {
|
public moveStartForwards(delta: number): void {
|
||||||
this._start = this._start.forwardsWhile(this.model, () => {
|
this._start = this._start.forwardsWhile(this.model, () => {
|
||||||
delta -= 1;
|
delta -= 1;
|
||||||
return delta >= 0;
|
return delta >= 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public moveEndBackwards(delta: number): void {
|
||||||
|
this._end = this._end.backwardsWhile(this.model, () => {
|
||||||
|
delta -= 1;
|
||||||
|
return delta >= 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public trim(): void {
|
public trim(): void {
|
||||||
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
||||||
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
||||||
|
|
|
@ -218,15 +218,28 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
||||||
|
|
||||||
it("Should identify the user to posthog if pseudonymous", async () => {
|
it("Should identify the user to posthog if pseudonymous", async () => {
|
||||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||||
await analytics.identifyUser("foo");
|
class FakeClient {
|
||||||
expect(fakePosthog.identify.mock.calls[0][0])
|
getAccountDataFromServer = jest.fn().mockResolvedValue(null);
|
||||||
.toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
|
setAccountData = jest.fn().mockResolvedValue({});
|
||||||
|
}
|
||||||
|
await analytics.identifyUser(new FakeClient(), () => "analytics_id" );
|
||||||
|
expect(fakePosthog.identify.mock.calls[0][0]).toBe("analytics_id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should not identify the user to posthog if anonymous", async () => {
|
it("Should not identify the user to posthog if anonymous", async () => {
|
||||||
analytics.setAnonymity(Anonymity.Anonymous);
|
analytics.setAnonymity(Anonymity.Anonymous);
|
||||||
await analytics.identifyUser("foo");
|
await analytics.identifyUser(null);
|
||||||
expect(fakePosthog.identify.mock.calls.length).toBe(0);
|
expect(fakePosthog.identify.mock.calls.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should identify using the server's analytics id if present", async () => {
|
||||||
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||||
|
class FakeClient {
|
||||||
|
getAccountDataFromServer = jest.fn().mockResolvedValue({ id: "existing_analytics_id" });
|
||||||
|
setAccountData = jest.fn().mockResolvedValue({});
|
||||||
|
}
|
||||||
|
await analytics.identifyUser(new FakeClient(), () => "new_analytics_id" );
|
||||||
|
expect(fakePosthog.identify.mock.calls[0][0]).toBe("existing_analytics_id");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue