Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Add jump to date functionality to date headers in timeline
Browse files Browse the repository at this point in the history
Part of MSC3030: matrix-org/matrix-spec-proposals#3030

Experimental Synapse implementation added in matrix-org/synapse#9445
  • Loading branch information
MadLittleMods committed Dec 9, 2021
1 parent 2b52e17 commit 594c7c7
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 19 deletions.
12 changes: 6 additions & 6 deletions res/css/views/context_menus/_IconizedContextMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,21 @@ limitations under the License.
}

// round the top corners of the top button for the hover effect to be bounded
&:first-child .mx_AccessibleButton:first-child {
&:first-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child {
border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu
}

// round the bottom corners of the bottom button for the hover effect to be bounded
&:last-child .mx_AccessibleButton:last-child {
&:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):last-child {
border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu
}

// round all corners of the only button for the hover effect to be bounded
&:first-child:last-child .mx_AccessibleButton:first-child:last-child {
&:first-child:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child:last-child {
border-radius: 8px; // radius matches .mx_ContextualMenu
}

.mx_AccessibleButton {
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
// pad the inside of the button so that the hover background is padded too
padding-top: 12px;
padding-bottom: 12px;
Expand Down Expand Up @@ -130,7 +130,7 @@ limitations under the License.
}

.mx_IconizedContextMenu_optionList_red {
.mx_AccessibleButton {
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
color: $alert !important;
}

Expand All @@ -148,7 +148,7 @@ limitations under the License.
}

.mx_IconizedContextMenu_active {
&.mx_AccessibleButton, .mx_AccessibleButton {
&.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind), .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
color: $accent !important;
}

Expand Down
1 change: 1 addition & 0 deletions res/css/views/elements/_Field.scss
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ limitations under the License.
transform 0.25s ease-out 0s,
background-color 0.25s ease-out 0s;
font-size: $font-10px;
line-height: normal;
transform: translateY(-13px);
padding: 0 2px;
background-color: $background;
Expand Down
34 changes: 34 additions & 0 deletions res/css/views/messages/_DateSeparator.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,37 @@ limitations under the License.
margin: 0 25px;
flex: 0 0 auto;
}

.mx_DateSeparator_jumpToDateMenu {
display: flex;
}

.mx_DateSeparator_chevron {
align-self: center;
width: 16px;
height: 16px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
background-color: $tertiary-content;
}

.mx_DateSeparator_jumpToDateMenuOption > .mx_IconizedContextMenu_label {
flex: initial;
width: auto;
}

.mx_DateSeparator_datePickerForm {
display: flex;
}

.mx_DateSeparator_datePicker {
flex: initial;
margin: 0;
margin-left: 8px;
}

.mx_DateSeparator_datePickerSubmitButton {
margin-left: 8px;
}
10 changes: 5 additions & 5 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {

// do we need a date separator since the last event?
const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate);
if (wantsDateSeparator && !isGrouped) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
if (wantsDateSeparator && !isGrouped && this.props.room) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} roomId={this.props.room.roomId} ts={ts1} /></li>;
ret.push(dateSeparator);
}

Expand Down Expand Up @@ -1109,7 +1109,7 @@ class CreationGrouper extends BaseGrouper {
if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
const ts = createEvent.getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={createEvent.getRoomId()} ts={ts} /></li>,
);
}

Expand Down Expand Up @@ -1222,7 +1222,7 @@ class RedactionGrouper extends BaseGrouper {
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
);
}

Expand Down Expand Up @@ -1318,7 +1318,7 @@ class MemberGrouper extends BaseGrouper {
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/MessageEditHistoryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
const baseEventId = this.props.mxEvent.getId();
allEvents.forEach((e, i) => {
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
nodes.push(<li key={e.getTs() + "~"}><DateSeparator roomId={e.getRoomId()} ts={e.getTs()} /></li>);
}
const isBaseEvent = e.getId() === baseEventId;
nodes.push((
Expand Down
193 changes: 189 additions & 4 deletions src/components/views/messages/DateSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import { formatFullDateNoTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';

import Field from "../elements/Field";
import Modal from '../../../Modal';
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from "../elements/AccessibleButton";
import { contextMenuBelow } from '../rooms/RoomTile';
import { ContextMenuTooltipButton } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";

function getDaysArray(): string[] {
return [
Expand All @@ -34,13 +50,26 @@ function getDaysArray(): string[] {
}

interface IProps {
roomId: string,
ts: number;
forExport?: boolean;
}

interface IState {
dateValue: string,
contextMenuPosition?: DOMRect
}

@replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component<IProps> {
private getLabel() {
export default class DateSeparator extends React.Component<IProps, IState> {
constructor(props, context) {
super(props, context);
this.state = {
dateValue: this.getDefaultDateValue()
};
}

private getLabel(): string {
const date = new Date(this.props.ts);

// During the time the archive is being viewed, a specific day might not make sense, so we return the full date
Expand All @@ -62,12 +91,168 @@ export default class DateSeparator extends React.Component<IProps> {
}
}

private getDefaultDateValue(): string {
const date = new Date(this.props.ts);
const year = date.getFullYear();
const month = `${date.getMonth() + 1}`.padStart(2, "0")
const day = `${date.getDate()}`.padStart(2, "0")

return `${year}-${month}-${day}`
}

private pickDate = async (inputTimestamp): Promise<void> => {
console.log('pickDate', inputTimestamp)

const unixTimestamp = new Date(inputTimestamp).getTime();

const cli = MatrixClientPeg.get();
try {
const roomId = this.props.roomId
const { event_id, origin_server_ts } = await cli.timestampToEvent(
roomId,
unixTimestamp,
Direction.Forward
);
console.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp}`)

dis.dispatch({
action: Action.ViewRoom,
event_id,
highlighted: true,
room_id: roomId,
});
} catch (e) {
const code = e.errcode || e.statusCode;
// only show the dialog if failing for something other than a network error
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
// detached queue and we show the room status bar to allow retry
if (typeof code !== "undefined") {
// display error message stating you couldn't delete this.
Modal.createTrackedDialog('Unable to find event at that date', '', ErrorDialog, {
title: _t('Error'),
description: _t('Unable to find event at that date. (%(code)s)', { code }),
});
}
}
};

private onDateValueChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void => {
this.setState({ dateValue: e.target.value });
};

private onContextMenuOpenClick = (ev: React.MouseEvent): void => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
};

private closeMenu = (): void => {
this.setState({
contextMenuPosition: null,
});
};

private onContextMenuCloseClick = (): void => {
this.closeMenu();
};

private onLastWeekClicked = (): void => {
const date = new Date();
// This just goes back 7 days.
// FIXME: Do we want this to go back to the last Sunday? https://upokary.com/how-to-get-last-monday-or-last-friday-or-any-last-day-in-javascript/
date.setDate(date.getDate() - 7);
this.pickDate(date);
this.closeMenu();
}

private onLastMonthClicked = (): void => {
const date = new Date();
// Month numbers are 0 - 11 and `setMonth` handles the negative rollover
date.setMonth(date.getMonth() - 1, 1);
this.pickDate(date);
this.closeMenu();
}

private onTheBeginningClicked = (): void => {
const date = new Date(0);
this.pickDate(date);
this.closeMenu();
}

private onJumpToDateSubmit = (): void => {
console.log('onJumpToDateSubmit')
this.pickDate(this.state.dateValue);
this.closeMenu();
}

private renderNotificationsMenu(): React.ReactElement {
let contextMenu: JSX.Element;
if (this.state.contextMenuPosition) {
contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
compact
onFinished={this.onContextMenuCloseClick}
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Last week")}
onClick={this.onLastWeekClicked}
/>
<IconizedContextMenuOption
label={_t("Last month")}
onClick={this.onLastMonthClicked}
/>
<IconizedContextMenuOption
label={_t("The beginning of the room")}
onClick={this.onTheBeginningClicked}
/>
</IconizedContextMenuOptionList>

<IconizedContextMenuOptionList>
<IconizedContextMenuOption
className="mx_DateSeparator_jumpToDateMenuOption"
label={_t("Jump to date")}
onClick={() => {}}
>
<form className="mx_DateSeparator_datePickerForm" onSubmit={this.onJumpToDateSubmit}>
<Field
type="date"
onChange={this.onDateValueChange}
value={this.state.dateValue}
className="mx_DateSeparator_datePicker"
label={_t("Pick a date to jump to")}
autoFocus={true}
/>
<AccessibleButton kind="primary" className="mx_DateSeparator_datePickerSubmitButton" onClick={this.onJumpToDateSubmit}>
{ _t("Go") }
</AccessibleButton>
</form>
</IconizedContextMenuOption>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}

return (
<ContextMenuTooltipButton
className="mx_DateSeparator_jumpToDateMenu"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("Jump to date")}
>
<div aria-hidden="true">{ this.getLabel() }</div>
<div className="mx_DateSeparator_chevron" />
{ contextMenu }
</ContextMenuTooltipButton>
);
}

render() {
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return <h2 className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={this.getLabel()}>
return <h2 className="mx_DateSeparator" role="separator" aria-label={this.getLabel()}>
<hr role="none" />
<div aria-hidden="true">{ this.getLabel() }</div>
{ this.renderNotificationsMenu() }
<hr role="none" />
</h2>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/SearchResultTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default class SearchResultTile extends React.Component<IProps> {
const eventId = mxEv.getId();

const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const ret = [<DateSeparator key={ts1 + "-search"} roomId={mxEv.getRoomId()} ts={ts1} />];
const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2026,6 +2026,13 @@
"Saturday": "Saturday",
"Today": "Today",
"Yesterday": "Yesterday",
"Unable to find event at that date. (%(code)s)": "Unable to find event at that date. (%(code)s)",
"Last week": "Last week",
"Last month": "Last month",
"The beginning of the room": "The beginning of the room",
"Jump to date": "Jump to date",
"Pick a date to jump to": "Pick a date to jump to",
"Go": "Go",
"Downloading": "Downloading",
"Decrypting": "Decrypting",
"Download": "Download",
Expand Down Expand Up @@ -2550,7 +2557,6 @@
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Start a conversation with someone using their name, email address or username (like <userId/>).",
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
"Go": "Go",
"Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
"If you can't see who you're looking for, send them your invite link below.": "If you can't see who you're looking for, send them your invite link below.",
"Or send invite link": "Or send invite link",
Expand Down
2 changes: 1 addition & 1 deletion src/utils/exportUtils/HtmlExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export default class HTMLExporter extends Exporter {

protected getDateSeparator(event: MatrixEvent) {
const ts = event.getTs();
const dateSeparator = <li key={ts}><DateSeparator forExport={true} key={ts} ts={ts} /></li>;
const dateSeparator = <li key={ts}><DateSeparator forExport={true} key={ts} roomId={event.getRoomId()} ts={ts} /></li>;
return renderToStaticMarkup(dateSeparator);
}

Expand Down

0 comments on commit 594c7c7

Please sign in to comment.