FIX: improves linking of thread messages (#26095)

- The thread preview is now a regular link and can be right clicked
- left gutter date, and regular date of a thread message will not correctly link to the thread's message
This commit is contained in:
Joffrey JAFFEUX
2024-03-08 09:09:42 +01:00
committed by GitHub
parent 57df0d526e
commit 21a7ebf1bc
7 changed files with 48 additions and 136 deletions

View File

@ -1,146 +1,33 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { LinkTo } from "@ember/routing";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class";
import formatDate from "discourse/helpers/format-date"; import formatDate from "discourse/helpers/format-date";
import replaceEmoji from "discourse/helpers/replace-emoji"; import replaceEmoji from "discourse/helpers/replace-emoji";
import htmlSafe from "discourse-common/helpers/html-safe"; import htmlSafe from "discourse-common/helpers/html-safe";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import getURL from "discourse-common/lib/get-url";
import { bind } from "discourse-common/utils/decorators";
import ChatThreadParticipants from "./chat-thread-participants"; import ChatThreadParticipants from "./chat-thread-participants";
import ChatUserAvatar from "./chat-user-avatar"; import ChatUserAvatar from "./chat-user-avatar";
export default class ChatMessageThreadIndicator extends Component { export default class ChatMessageThreadIndicator extends Component {
@service capabilities;
@service chat;
@service chatStateManager;
@service router;
@service site;
@tracked isActive = false;
get interactiveUser() { get interactiveUser() {
return this.args.interactiveUser ?? true; return this.args.interactiveUser ?? true;
} }
@action get threadMessageRoute() {
setup(element) { return [
this.element = element; ...this.args.message.thread.routeModels,
this.args.message.thread.preview.lastReplyId,
if (this.capabilities.touch) { ];
this.element.addEventListener("touchstart", this.onTouchStart, {
passive: true,
});
this.element.addEventListener("touchmove", this.cancelTouch, {
passive: true,
});
this.element.addEventListener("touchend", this.onTouchEnd);
this.element.addEventListener("touchCancel", this.cancelTouch);
}
this.element.addEventListener("mousedown", this.openThread, {
passive: true,
});
this.element.addEventListener("keydown", this.openThread, {
passive: true,
});
}
@action
teardown() {
if (this.capabilities.touch) {
this.element.removeEventListener("touchstart", this.onTouchStart, {
passive: true,
});
this.element.removeEventListener("touchmove", this.cancelTouch, {
passive: true,
});
this.element.removeEventListener("touchend", this.onTouchEnd);
this.element.removeEventListener("touchCancel", this.cancelTouch);
}
this.element.removeEventListener("mousedown", this.openThread, {
passive: true,
});
this.element.removeEventListener("keydown", this.openThread, {
passive: true,
});
}
@bind
onTouchStart(event) {
this.isActive = true;
event.stopPropagation();
this.touching = true;
}
@bind
onTouchEnd() {
this.isActive = false;
if (this.touching) {
this.openThread();
}
}
@bind
cancelTouch() {
this.isActive = false;
this.touching = false;
}
@bind
openThread(event) {
if (event?.type === "keydown" && event?.key !== "Enter") {
return;
}
// handle middle mouse
if (
event?.type === "mousedown" &&
(event?.which === 2 || event?.shiftKey)
) {
window.open(
getURL(
this.router.urlFor(
"chat.channel.thread",
...this.args.message.thread.routeModels
)
),
"_blank"
);
return;
}
this.chat.activeMessage = null;
this.router.transitionTo(
"chat.channel.thread",
...this.args.message.thread.routeModels
);
} }
<template> <template>
<div <LinkTo
class={{concatClass class="chat-message-thread-indicator"
"chat-message-thread-indicator" @route="chat.channel.thread.near-message"
(if this.isActive "-active") @models={{this.threadMessageRoute}}
}}
{{didInsert this.setup}}
{{willDestroy this.teardown}}
role="button"
title={{i18n "chat.threads.open"}} title={{i18n "chat.threads.open"}}
tabindex="0" tabindex="0"
...attributes ...attributes
> >
<div class="chat-message-thread-indicator__last-reply-avatar"> <div class="chat-message-thread-indicator__last-reply-avatar">
<ChatUserAvatar <ChatUserAvatar
@user={{@message.thread.preview.lastReplyUser}} @user={{@message.thread.preview.lastReplyUser}}
@ -167,6 +54,6 @@ export default class ChatMessageThreadIndicator extends Component {
<div class="chat-message-thread-indicator__last-reply-excerpt"> <div class="chat-message-thread-indicator__last-reply-excerpt">
{{replaceEmoji (htmlSafe @message.thread.preview.lastReplyExcerpt)}} {{replaceEmoji (htmlSafe @message.thread.preview.lastReplyExcerpt)}}
</div> </div>
</div> </LinkTo>
</template> </template>
} }

View File

@ -589,7 +589,10 @@ export default class ChatMessage extends Component {
{{/unless}} {{/unless}}
{{#if this.hideUserInfo}} {{#if this.hideUserInfo}}
<ChatMessageLeftGutter @message={{@message}} /> <ChatMessageLeftGutter
@message={{@message}}
@threadContext={{this.threadContext}}
/>
{{else}} {{else}}
<ChatMessageAvatar @message={{@message}} /> <ChatMessageAvatar @message={{@message}} />
{{/if}} {{/if}}

View File

@ -1,4 +1,5 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy"; import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
@ -142,7 +143,7 @@ export default class ChatMessageInfo extends Component {
{{/if}} {{/if}}
<span class="chat-message-info__date"> <span class="chat-message-info__date">
{{formatChatDate @message}} {{formatChatDate @message (hash threadContext=@threadContext)}}
</span> </span>
{{#if @message.bookmark}} {{#if @message.bookmark}}

View File

@ -1,4 +1,5 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { eq } from "truth-helpers"; import { eq } from "truth-helpers";
@ -25,7 +26,10 @@ export default class ChatMessageLeftGutter extends Component {
</div> </div>
{{else if this.site.desktopView}} {{else if this.site.desktopView}}
<span class="chat-message-left-gutter__date"> <span class="chat-message-left-gutter__date">
{{formatChatDate @message "tiny"}} {{formatChatDate
@message
(hash mode="tiny" threadContext=@threadContext)
}}
</span> </span>
{{/if}} {{/if}}
{{#if @message.bookmark}} {{#if @message.bookmark}}

View File

@ -3,14 +3,14 @@ import User from "discourse/models/user";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
export default function formatChatDate(message, mode) { export default function formatChatDate(message, options = {}) {
const currentUser = User.current(); const currentUser = User.current();
const tz = currentUser ? currentUser.user_option.timezone : moment.tz.guess(); const tz = currentUser ? currentUser.user_option.timezone : moment.tz.guess();
const date = moment(new Date(message.createdAt), tz); const date = moment(new Date(message.createdAt), tz);
const title = date.format(I18n.t("dates.long_with_year")); const title = date.format(I18n.t("dates.long_with_year"));
const display = const display =
mode === "tiny" options.mode === "tiny"
? date.format(I18n.t("chat.dates.time_tiny")) ? date.format(I18n.t("chat.dates.time_tiny"))
: date.format(I18n.t("dates.time")); : date.format(I18n.t("dates.time"));
@ -19,7 +19,15 @@ export default function formatChatDate(message, mode) {
`<span title='${title}' tabindex="-1" class='chat-time'>${display}</span>` `<span title='${title}' tabindex="-1" class='chat-time'>${display}</span>`
); );
} else { } else {
const url = getURL(`/chat/c/-/${message.channel.id}/${message.id}`); let url;
if (options.threadContext) {
url = getURL(
`/chat/c/-/${message.channel.id}/t/${message.thread.id}/${message.id}`
);
} else {
url = getURL(`/chat/c/-/${message.channel.id}/${message.id}`);
}
return htmlSafe( return htmlSafe(
`<a title='${title}' tabindex="-1" class='chat-time' href='${url}'>${display}</a>` `<a title='${title}' tabindex="-1" class='chat-time' href='${url}'>${display}</a>`
); );

View File

@ -29,12 +29,6 @@
} }
} }
.touch & {
&.-active {
box-shadow: var(--shadow-dropdown);
}
}
.no-touch & { .no-touch & {
&:hover { &:hover {
box-shadow: var(--shadow-dropdown); box-shadow: var(--shadow-dropdown);

View File

@ -19,4 +19,19 @@ module("Discourse Chat | Unit | Helpers | format-chat-date", function (hooks) {
`/chat/c/-/${channel.id}/${this.message.id}` `/chat/c/-/${channel.id}/${this.message.id}`
); );
}); });
test("link to chat message thread", async function (assert) {
const channel = fabricators.channel();
const thread = fabricators.thread();
this.message = fabricators.message({ channel, thread });
await render(
hbs`{{format-chat-date this.message (hash threadContext=true)}}`
);
assert.equal(
query(".chat-time").getAttribute("href"),
`/chat/c/-/${channel.id}/t/${thread.id}/${this.message.id}`
);
});
}); });