Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion res/css/components/views/messages/_MBeaconBody.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ limitations under the License.
.mx_MBeaconBody {
position: relative;
height: 220px;
width: 325px;
max-width: 325px;
width: 100%;

border-radius: $timeline-image-border-radius;
overflow: hidden;
Expand Down
16 changes: 12 additions & 4 deletions src/components/views/beacon/OwnBeaconStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
import { useOwnLiveBeacons } from '../../../utils/beacon';
import BeaconStatus from './BeaconStatus';
import { BeaconDisplayStatus } from './displayStatus';
import AccessibleButton from '../elements/AccessibleButton';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';

interface Props {
displayStatus: BeaconDisplayStatus;
Expand All @@ -45,6 +45,14 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
onResetLocationPublishError,
} = useOwnLiveBeacons([beacon?.identifier]);

// eat events here to avoid 1) the map and 2) reply or thread tiles
// moving under the beacon status on stop/retry click
const preventDefaultWrapper = (callback: () => void) => (e?: ButtonEvent) => {
e?.stopPropagation();
e?.preventDefault();
callback();
};

// combine display status with errors that only occur for user's own beacons
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
BeaconDisplayStatus.Error :
Expand All @@ -60,7 +68,7 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
data-test-id='beacon-status-stop-beacon'
kind='link'
onClick={onStopSharing}
onClick={preventDefaultWrapper(onStopSharing)}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
disabled={stoppingInProgress}
>
Expand All @@ -70,7 +78,7 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
{ hasLocationPublishError && <AccessibleButton
data-test-id='beacon-status-reset-wire-error'
kind='link'
onClick={onResetLocationPublishError}
onClick={preventDefaultWrapper(onResetLocationPublishError)}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
>
{ _t('Retry') }
Expand All @@ -79,7 +87,7 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
{ hasStopSharingError && <AccessibleButton
data-test-id='beacon-status-stop-beacon-retry'
kind='link'
onClick={onStopSharing}
onClick={preventDefaultWrapper(onStopSharing)}
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
>
{ _t('Retry') }
Expand Down
11 changes: 9 additions & 2 deletions src/components/views/messages/MessageActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/mo
import classNames from 'classnames';
import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event';
import { Thread } from 'matrix-js-sdk/src/models/thread';
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';

import type { Relations } from 'matrix-js-sdk/src/models/relations';
import { _t } from '../../../languageHandler';
Expand Down Expand Up @@ -329,8 +330,14 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction

const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;

const isAllowedMessageType = !this.forbiddenThreadHeadMsgType.includes(
this.props.mxEvent.getContent().msgtype as MsgType,
const isAllowedMessageType = (
!this.forbiddenThreadHeadMsgType.includes(
this.props.mxEvent.getContent().msgtype as MsgType) &&
/** forbid threads from live location shares
* until cross-platform support
* (PSF-1041)
*/
!M_BEACON_INFO.matches(this.props.mxEvent.getType())
);

return inNotThreadTimeline && isAllowedMessageType;
Expand Down
8 changes: 6 additions & 2 deletions src/utils/EventUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MatrixClient } from 'matrix-js-sdk/src/client';
import { logger } from 'matrix-js-sdk/src/logger';
import { M_POLL_START } from "matrix-events-sdk";
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';

import { MatrixClientPeg } from '../MatrixClientPeg';
import shouldHideEvent from "../shouldHideEvent";
Expand Down Expand Up @@ -52,7 +53,8 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
}
} else if (
mxEvent.getType() === 'm.sticker' ||
M_POLL_START.matches(mxEvent.getType())
M_POLL_START.matches(mxEvent.getType()) ||
M_BEACON_INFO.matches(mxEvent.getType())
) {
return true;
}
Expand Down Expand Up @@ -277,7 +279,9 @@ export const isLocationEvent = (event: MatrixEvent): boolean => {

export function canForward(event: MatrixEvent): boolean {
return !(
M_POLL_START.matches(event.getType())
M_POLL_START.matches(event.getType()) ||
// disallow forwarding until psf-1044
M_BEACON_INFO.matches(event.getType())
);
}

Expand Down
104 changes: 104 additions & 0 deletions test/components/views/messages/MessageActionBar-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,28 @@ import {
MsgType,
Room,
} from 'matrix-js-sdk/src/matrix';
import { Thread } from 'matrix-js-sdk/src/models/thread';

import MessageActionBar from '../../../../src/components/views/messages/MessageActionBar';
import {
getMockClientWithEventEmitter,
mockClientMethodsUser,
mockClientMethodsEvents,
makeBeaconInfoEvent,
} from '../../../test-utils';
import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks';
import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/RoomContext';
import { IRoomState } from '../../../../src/components/structures/RoomView';
import dispatcher from '../../../../src/dispatcher/dispatcher';
import SettingsStore from '../../../../src/settings/SettingsStore';
import { Action } from '../../../../src/dispatcher/actions';
import { UserTab } from '../../../../src/components/views/dialogs/UserTab';
import { showThread } from '../../../../src/dispatcher/dispatch-actions/threads';

jest.mock('../../../../src/dispatcher/dispatcher');
jest.mock('../../../../src/dispatcher/dispatch-actions/threads', () => ({
showThread: jest.fn(),
}));

describe('<MessageActionBar />', () => {
const userId = '@alice:server.org';
Expand Down Expand Up @@ -360,4 +368,100 @@ describe('<MessageActionBar />', () => {
it.todo('unsends event on cancel click');
it.todo('retrys event on retry click');
});

describe('thread button', () => {
beforeEach(() => {
Thread.setServerSideSupport(true, false);
});

describe('when threads feature is not enabled', () => {
it('does not render thread button when threads does not have server support', () => {
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
Thread.setServerSideSupport(false, false);
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
expect(queryByLabelText('Reply in thread')).toBeFalsy();
});

it('renders thread button when threads has server support', () => {
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
expect(queryByLabelText('Reply in thread')).toBeTruthy();
});

it('opens user settings on click', () => {
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent });

act(() => {
fireEvent.click(getByLabelText('Reply in thread'));
});

expect(dispatcher.dispatch).toHaveBeenCalledWith({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
});
});

describe('when threads feature is enabled', () => {
beforeEach(() => {
jest.spyOn(SettingsStore, 'getValue').mockImplementation(setting => setting === 'feature_thread');
});

it('renders thread button on own actionable event', () => {
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
expect(queryByLabelText('Reply in thread')).toBeTruthy();
});

it('does not render thread button for a beacon_info event', () => {
const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId);
const { queryByLabelText } = getComponent({ mxEvent: beaconInfoEvent });
expect(queryByLabelText('Reply in thread')).toBeFalsy();
});

it('opens thread on click', () => {
const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent });

act(() => {
fireEvent.click(getByLabelText('Reply in thread'));
});

expect(showThread).toHaveBeenCalledWith({
rootEvent: alicesMessageEvent,
push: false,
});
});

it('opens parent thread for a thread reply message', () => {
const threadReplyEvent = new MatrixEvent({
type: EventType.RoomMessage,
sender: userId,
room_id: roomId,
content: {
msgtype: MsgType.Text,
body: 'this is a thread reply',
},
});
// mock the thread stuff
jest.spyOn(threadReplyEvent, 'isThreadRelation', 'get').mockReturnValue(true);
// set alicesMessageEvent as the root event
jest.spyOn(threadReplyEvent, 'getThread').mockReturnValue(
{ rootEvent: alicesMessageEvent } as unknown as Thread,
);
const { getByLabelText } = getComponent({ mxEvent: threadReplyEvent });

act(() => {
fireEvent.click(getByLabelText('Reply in thread'));
});

expect(showThread).toHaveBeenCalledWith({
rootEvent: alicesMessageEvent,
initialEvent: threadReplyEvent,
highlighted: true,
scroll_into_view: true,
push: false,
});
});
});
});
});
11 changes: 10 additions & 1 deletion test/utils/EventUtils-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ describe('EventUtils', () => {
});
redactedEvent.makeRedacted(redactedEvent);

const stateEvent = makeBeaconInfoEvent(userId, roomId);
const stateEvent = new MatrixEvent({
type: EventType.RoomTopic,
state_key: '',
});
const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId);

const roomMemberEvent = new MatrixEvent({
type: EventType.RoomMember,
Expand Down Expand Up @@ -155,6 +159,7 @@ describe('EventUtils', () => {
['poll start event', pollStartEvent],
['event with empty content body', emptyContentBody],
['event with a content body', niceTextMessage],
['beacon_info event', beaconInfoEvent],
])('returns true for %s', (_description, event) => {
expect(isContentActionable(event)).toBe(true);
});
Expand Down Expand Up @@ -325,6 +330,10 @@ describe('EventUtils', () => {
const event = makePollStartEvent('Who?', userId);
expect(canForward(event)).toBe(false);
});
it('returns false for a beacon_info event', () => {
const event = makeBeaconInfoEvent(userId, roomId);
expect(canForward(event)).toBe(false);
});
it('returns true for a room message event', () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
Expand Down