diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 56e98fa50ec..9dfda3b013a 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -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; @@ -130,7 +130,7 @@ limitations under the License. } .mx_IconizedContextMenu_optionList_red { - .mx_AccessibleButton { + .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { color: $alert !important; } @@ -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; } diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index a97e7ee949e..1083a324fe2 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -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; diff --git a/res/css/views/messages/_DateSeparator.scss b/res/css/views/messages/_DateSeparator.scss index 66501b40cb3..bd9b77227db 100644 --- a/res/css/views/messages/_DateSeparator.scss +++ b/res/css/views/messages/_DateSeparator.scss @@ -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; +} diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 60a0829e2de..afada494cfd 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -715,8 +715,8 @@ export default class MessagePanel extends React.Component { // do we need a date separator since the last event? const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator && !isGrouped) { - const dateSeparator =
  • ; + if (wantsDateSeparator && !isGrouped && this.props.room) { + const dateSeparator =
  • ; ret.push(dateSeparator); } @@ -1109,7 +1109,7 @@ class CreationGrouper extends BaseGrouper { if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { const ts = createEvent.getTs(); ret.push( -
  • , +
  • , ); } @@ -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( -
  • , +
  • , ); } @@ -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( -
  • , +
  • , ); } diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx index f5a990e4091..df86c80fc6e 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx +++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx @@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { - nodes.push(
  • ); + nodes.push(
  • ); } const isBaseEvent = e.getId() === baseEventId; nodes.push(( diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index b20319e800e..03da621156d 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -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 [ @@ -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 { - private getLabel() { +export default class DateSeparator extends React.Component { + 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 @@ -62,12 +91,168 @@ export default class DateSeparator extends React.Component { } } + 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 => { + 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): 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 = + + + + + + + + {}} + > +
    + + + { _t("Go") } + + +
    +
    +
    ; + } + + return ( + + +
    + { contextMenu } + + ); + } + render() { // ARIA treats
    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

    + return


    - + { this.renderNotificationsMenu() }

    ; } diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index c033855eb54..b99cafbc490 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -46,7 +46,7 @@ export default class SearchResultTile extends React.Component { const eventId = mxEv.getId(); const ts1 = mxEv.getTs(); - const ret = []; + const ret = []; const layout = SettingsStore.getValue("layout"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5349fd5224e..a055d088054 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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", @@ -2550,7 +2557,6 @@ "Start a conversation with someone using their name, email address or username (like ).": "Start a conversation with someone using their name, email address or username (like ).", "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", - "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", diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 7c8265fd32c..03769d387cd 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -247,7 +247,7 @@ export default class HTMLExporter extends Exporter { protected getDateSeparator(event: MatrixEvent) { const ts = event.getTs(); - const dateSeparator =
  • ; + const dateSeparator =
  • ; return renderToStaticMarkup(dateSeparator); }