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

Commit 594c7c7

Browse files
committed
Add jump to date functionality to date headers in timeline
Part of MSC3030: matrix-org/matrix-spec-proposals#3030 Experimental Synapse implementation added in matrix-org/synapse#9445
1 parent 2b52e17 commit 594c7c7

File tree

9 files changed

+245
-19
lines changed

9 files changed

+245
-19
lines changed

res/css/views/context_menus/_IconizedContextMenu.scss

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,21 @@ limitations under the License.
5050
}
5151

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

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

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

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

132132
.mx_IconizedContextMenu_optionList_red {
133-
.mx_AccessibleButton {
133+
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
134134
color: $alert !important;
135135
}
136136

@@ -148,7 +148,7 @@ limitations under the License.
148148
}
149149

150150
.mx_IconizedContextMenu_active {
151-
&.mx_AccessibleButton, .mx_AccessibleButton {
151+
&.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind), .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
152152
color: $accent !important;
153153
}
154154

res/css/views/elements/_Field.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ limitations under the License.
127127
transform 0.25s ease-out 0s,
128128
background-color 0.25s ease-out 0s;
129129
font-size: $font-10px;
130+
line-height: normal;
130131
transform: translateY(-13px);
131132
padding: 0 2px;
132133
background-color: $background;

res/css/views/messages/_DateSeparator.scss

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,37 @@ limitations under the License.
3333
margin: 0 25px;
3434
flex: 0 0 auto;
3535
}
36+
37+
.mx_DateSeparator_jumpToDateMenu {
38+
display: flex;
39+
}
40+
41+
.mx_DateSeparator_chevron {
42+
align-self: center;
43+
width: 16px;
44+
height: 16px;
45+
mask-position: center;
46+
mask-size: contain;
47+
mask-repeat: no-repeat;
48+
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
49+
background-color: $tertiary-content;
50+
}
51+
52+
.mx_DateSeparator_jumpToDateMenuOption > .mx_IconizedContextMenu_label {
53+
flex: initial;
54+
width: auto;
55+
}
56+
57+
.mx_DateSeparator_datePickerForm {
58+
display: flex;
59+
}
60+
61+
.mx_DateSeparator_datePicker {
62+
flex: initial;
63+
margin: 0;
64+
margin-left: 8px;
65+
}
66+
67+
.mx_DateSeparator_datePickerSubmitButton {
68+
margin-left: 8px;
69+
}

src/components/structures/MessagePanel.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
715715

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

@@ -1109,7 +1109,7 @@ class CreationGrouper extends BaseGrouper {
11091109
if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
11101110
const ts = createEvent.getTs();
11111111
ret.push(
1112-
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
1112+
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={createEvent.getRoomId()} ts={ts} /></li>,
11131113
);
11141114
}
11151115

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

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

src/components/views/dialogs/MessageEditHistoryDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
130130
const baseEventId = this.props.mxEvent.getId();
131131
allEvents.forEach((e, i) => {
132132
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
133-
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
133+
nodes.push(<li key={e.getTs() + "~"}><DateSeparator roomId={e.getRoomId()} ts={e.getTs()} /></li>);
134134
}
135135
const isBaseEvent = e.getId() === baseEventId;
136136
nodes.push((

src/components/views/messages/DateSeparator.tsx

Lines changed: 189 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ import React from 'react';
2020
import { _t } from '../../../languageHandler';
2121
import { formatFullDateNoTime } from '../../../DateUtils';
2222
import { replaceableComponent } from "../../../utils/replaceableComponent";
23+
import { MatrixClientPeg } from '../../../MatrixClientPeg';
24+
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
25+
import dis from '../../../dispatcher/dispatcher';
26+
import { Action } from '../../../dispatcher/actions';
27+
28+
import Field from "../elements/Field";
29+
import Modal from '../../../Modal';
30+
import ErrorDialog from '../dialogs/ErrorDialog';
31+
import AccessibleButton from "../elements/AccessibleButton";
32+
import { contextMenuBelow } from '../rooms/RoomTile';
33+
import { ContextMenuTooltipButton } from "../../structures/ContextMenu";
34+
import IconizedContextMenu, {
35+
IconizedContextMenuOption,
36+
IconizedContextMenuOptionList,
37+
IconizedContextMenuRadio,
38+
} from "../context_menus/IconizedContextMenu";
2339

2440
function getDaysArray(): string[] {
2541
return [
@@ -34,13 +50,26 @@ function getDaysArray(): string[] {
3450
}
3551

3652
interface IProps {
53+
roomId: string,
3754
ts: number;
3855
forExport?: boolean;
3956
}
4057

58+
interface IState {
59+
dateValue: string,
60+
contextMenuPosition?: DOMRect
61+
}
62+
4163
@replaceableComponent("views.messages.DateSeparator")
42-
export default class DateSeparator extends React.Component<IProps> {
43-
private getLabel() {
64+
export default class DateSeparator extends React.Component<IProps, IState> {
65+
constructor(props, context) {
66+
super(props, context);
67+
this.state = {
68+
dateValue: this.getDefaultDateValue()
69+
};
70+
}
71+
72+
private getLabel(): string {
4473
const date = new Date(this.props.ts);
4574

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

94+
private getDefaultDateValue(): string {
95+
const date = new Date(this.props.ts);
96+
const year = date.getFullYear();
97+
const month = `${date.getMonth() + 1}`.padStart(2, "0")
98+
const day = `${date.getDate()}`.padStart(2, "0")
99+
100+
return `${year}-${month}-${day}`
101+
}
102+
103+
private pickDate = async (inputTimestamp): Promise<void> => {
104+
console.log('pickDate', inputTimestamp)
105+
106+
const unixTimestamp = new Date(inputTimestamp).getTime();
107+
108+
const cli = MatrixClientPeg.get();
109+
try {
110+
const roomId = this.props.roomId
111+
const { event_id, origin_server_ts } = await cli.timestampToEvent(
112+
roomId,
113+
unixTimestamp,
114+
Direction.Forward
115+
);
116+
console.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp}`)
117+
118+
dis.dispatch({
119+
action: Action.ViewRoom,
120+
event_id,
121+
highlighted: true,
122+
room_id: roomId,
123+
});
124+
} catch (e) {
125+
const code = e.errcode || e.statusCode;
126+
// only show the dialog if failing for something other than a network error
127+
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
128+
// detached queue and we show the room status bar to allow retry
129+
if (typeof code !== "undefined") {
130+
// display error message stating you couldn't delete this.
131+
Modal.createTrackedDialog('Unable to find event at that date', '', ErrorDialog, {
132+
title: _t('Error'),
133+
description: _t('Unable to find event at that date. (%(code)s)', { code }),
134+
});
135+
}
136+
}
137+
};
138+
139+
private onDateValueChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void => {
140+
this.setState({ dateValue: e.target.value });
141+
};
142+
143+
private onContextMenuOpenClick = (ev: React.MouseEvent): void => {
144+
ev.preventDefault();
145+
ev.stopPropagation();
146+
const target = ev.target as HTMLButtonElement;
147+
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
148+
};
149+
150+
private closeMenu = (): void => {
151+
this.setState({
152+
contextMenuPosition: null,
153+
});
154+
};
155+
156+
private onContextMenuCloseClick = (): void => {
157+
this.closeMenu();
158+
};
159+
160+
private onLastWeekClicked = (): void => {
161+
const date = new Date();
162+
// This just goes back 7 days.
163+
// 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/
164+
date.setDate(date.getDate() - 7);
165+
this.pickDate(date);
166+
this.closeMenu();
167+
}
168+
169+
private onLastMonthClicked = (): void => {
170+
const date = new Date();
171+
// Month numbers are 0 - 11 and `setMonth` handles the negative rollover
172+
date.setMonth(date.getMonth() - 1, 1);
173+
this.pickDate(date);
174+
this.closeMenu();
175+
}
176+
177+
private onTheBeginningClicked = (): void => {
178+
const date = new Date(0);
179+
this.pickDate(date);
180+
this.closeMenu();
181+
}
182+
183+
private onJumpToDateSubmit = (): void => {
184+
console.log('onJumpToDateSubmit')
185+
this.pickDate(this.state.dateValue);
186+
this.closeMenu();
187+
}
188+
189+
private renderNotificationsMenu(): React.ReactElement {
190+
let contextMenu: JSX.Element;
191+
if (this.state.contextMenuPosition) {
192+
contextMenu = <IconizedContextMenu
193+
{...contextMenuBelow(this.state.contextMenuPosition)}
194+
compact
195+
onFinished={this.onContextMenuCloseClick}
196+
>
197+
<IconizedContextMenuOptionList first>
198+
<IconizedContextMenuOption
199+
label={_t("Last week")}
200+
onClick={this.onLastWeekClicked}
201+
/>
202+
<IconizedContextMenuOption
203+
label={_t("Last month")}
204+
onClick={this.onLastMonthClicked}
205+
/>
206+
<IconizedContextMenuOption
207+
label={_t("The beginning of the room")}
208+
onClick={this.onTheBeginningClicked}
209+
/>
210+
</IconizedContextMenuOptionList>
211+
212+
<IconizedContextMenuOptionList>
213+
<IconizedContextMenuOption
214+
className="mx_DateSeparator_jumpToDateMenuOption"
215+
label={_t("Jump to date")}
216+
onClick={() => {}}
217+
>
218+
<form className="mx_DateSeparator_datePickerForm" onSubmit={this.onJumpToDateSubmit}>
219+
<Field
220+
type="date"
221+
onChange={this.onDateValueChange}
222+
value={this.state.dateValue}
223+
className="mx_DateSeparator_datePicker"
224+
label={_t("Pick a date to jump to")}
225+
autoFocus={true}
226+
/>
227+
<AccessibleButton kind="primary" className="mx_DateSeparator_datePickerSubmitButton" onClick={this.onJumpToDateSubmit}>
228+
{ _t("Go") }
229+
</AccessibleButton>
230+
</form>
231+
</IconizedContextMenuOption>
232+
</IconizedContextMenuOptionList>
233+
</IconizedContextMenu>;
234+
}
235+
236+
return (
237+
<ContextMenuTooltipButton
238+
className="mx_DateSeparator_jumpToDateMenu"
239+
onClick={this.onContextMenuOpenClick}
240+
isExpanded={!!this.state.contextMenuPosition}
241+
title={_t("Jump to date")}
242+
>
243+
<div aria-hidden="true">{ this.getLabel() }</div>
244+
<div className="mx_DateSeparator_chevron" />
245+
{ contextMenu }
246+
</ContextMenuTooltipButton>
247+
);
248+
}
249+
65250
render() {
66251
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
67252
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
68-
return <h2 className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={this.getLabel()}>
253+
return <h2 className="mx_DateSeparator" role="separator" aria-label={this.getLabel()}>
69254
<hr role="none" />
70-
<div aria-hidden="true">{ this.getLabel() }</div>
255+
{ this.renderNotificationsMenu() }
71256
<hr role="none" />
72257
</h2>;
73258
}

src/components/views/rooms/SearchResultTile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default class SearchResultTile extends React.Component<IProps> {
4646
const eventId = mxEv.getId();
4747

4848
const ts1 = mxEv.getTs();
49-
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
49+
const ret = [<DateSeparator key={ts1 + "-search"} roomId={mxEv.getRoomId()} ts={ts1} />];
5050
const layout = SettingsStore.getValue("layout");
5151
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
5252
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");

src/i18n/strings/en_EN.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2026,6 +2026,13 @@
20262026
"Saturday": "Saturday",
20272027
"Today": "Today",
20282028
"Yesterday": "Yesterday",
2029+
"Unable to find event at that date. (%(code)s)": "Unable to find event at that date. (%(code)s)",
2030+
"Last week": "Last week",
2031+
"Last month": "Last month",
2032+
"The beginning of the room": "The beginning of the room",
2033+
"Jump to date": "Jump to date",
2034+
"Pick a date to jump to": "Pick a date to jump to",
2035+
"Go": "Go",
20292036
"Downloading": "Downloading",
20302037
"Decrypting": "Decrypting",
20312038
"Download": "Download",
@@ -2550,7 +2557,6 @@
25502557
"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/>).",
25512558
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
25522559
"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>",
2553-
"Go": "Go",
25542560
"Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
25552561
"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.",
25562562
"Or send invite link": "Or send invite link",

src/utils/exportUtils/HtmlExport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export default class HTMLExporter extends Exporter {
247247

248248
protected getDateSeparator(event: MatrixEvent) {
249249
const ts = event.getTs();
250-
const dateSeparator = <li key={ts}><DateSeparator forExport={true} key={ts} ts={ts} /></li>;
250+
const dateSeparator = <li key={ts}><DateSeparator forExport={true} key={ts} roomId={event.getRoomId()} ts={ts} /></li>;
251251
return renderToStaticMarkup(dateSeparator);
252252
}
253253

0 commit comments

Comments
 (0)