FIX: makes chat user avatar show presence by default (#22490)

It's way more common to have presence enabled than disabled, so we should have been making it the default from start.

This commit also changes the namespace of `<ChatUserAvatar />` into `<Chat::UserAvatar />` and refactors tests.
This commit is contained in:
Joffrey JAFFEUX
2023-07-10 09:36:20 +02:00
committed by GitHub
parent 81a16a105e
commit 03e495186f
23 changed files with 121 additions and 114 deletions

View File

@ -7,7 +7,7 @@
{{@channel.chatable.users.length}} {{@channel.chatable.users.length}}
</span> </span>
{{else}} {{else}}
<ChatUserAvatar @user={{@channel.chatable.users.firstObject}} /> <Chat::UserAvatar @user={{@channel.chatable.users.firstObject}} />
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}

View File

@ -5,7 +5,7 @@
> >
<div class="chat-reply"> <div class="chat-reply">
{{d-icon (if @message.editing "pencil-alt" "reply")}} {{d-icon (if @message.editing "pencil-alt" "reply")}}
<ChatUserAvatar @user={{@message.user}} /> <Chat::UserAvatar @user={{@message.user}} />
<span class="chat-reply__username">{{@message.user.username}}</span> <span class="chat-reply__username">{{@message.user.username}}</span>
<span class="chat-reply__excerpt"> <span class="chat-reply__excerpt">
{{replace-emoji @message.excerpt}} {{replace-emoji @message.excerpt}}

View File

@ -16,7 +16,7 @@
<div class="chat-message-actions"> <div class="chat-message-actions">
<div class="selected-message-container"> <div class="selected-message-container">
<div class="selected-message"> <div class="selected-message">
<ChatUserAvatar @user={{this.message.user}} /> <Chat::UserAvatar @user={{this.message.user}} />
<span <span
{{on "touchstart" this.expandReply passive=true}} {{on "touchstart" this.expandReply passive=true}}
role="button" role="button"

View File

@ -9,7 +9,7 @@
{{#if @message.inReplyTo.chatWebhookEvent.emoji}} {{#if @message.inReplyTo.chatWebhookEvent.emoji}}
<ChatEmojiAvatar @emoji={{@message.inReplyTo.chatWebhookEvent.emoji}} /> <ChatEmojiAvatar @emoji={{@message.inReplyTo.chatWebhookEvent.emoji}} />
{{else}} {{else}}
<ChatUserAvatar @user={{@message.inReplyTo.user}} /> <Chat::UserAvatar @user={{@message.inReplyTo.user}} />
{{/if}} {{/if}}
<span class="chat-reply__excerpt"> <span class="chat-reply__excerpt">

View File

@ -10,7 +10,7 @@
> >
<div class="chat-message-thread-indicator__last-reply-avatar"> <div class="chat-message-thread-indicator__last-reply-avatar">
<ChatUserAvatar <Chat::UserAvatar
@user={{@message.thread.preview.lastReplyUser}} @user={{@message.thread.preview.lastReplyUser}}
@avatarSize="small" @avatarSize="small"
/> />

View File

@ -1,11 +0,0 @@
<div
class="chat-user-avatar {{if (and this.isOnline @showPresence) 'is-online'}}"
>
<div
role="button"
class="chat-user-avatar-container clickable"
data-user-card={{@user.username}}
>
{{avatar @user imageSize=this.avatarSize}}
</div>
</div>

View File

@ -1,6 +1,6 @@
{{#if @user}} {{#if @user}}
<a href={{this.userPath}} data-user-card={{@user.username}}> <a href={{this.userPath}} data-user-card={{@user.username}}>
<ChatUserAvatar @user={{@user}} @avatarSize="medium" /> <Chat::UserAvatar @user={{@user}} @avatarSize="medium" />
</a> </a>
<a href={{this.userPath}} data-user-card={{@user.username}}> <a href={{this.userPath}} data-user-card={{@user.username}}>
<ChatUserDisplayName @user={{@user}} /> <ChatUserDisplayName @user={{@user}} />

View File

@ -1,4 +1,4 @@
<ChatUserAvatar @user={{@content.model}} @showPresence={{true}} /> <Chat::UserAvatar @user={{@content.model}} />
<ChatUserDisplayName @user={{@content.model}} /> <ChatUserDisplayName @user={{@content.model}} />
{{#if (gt @content.tracking.unreadCount 0)}} {{#if (gt @content.tracking.unreadCount 0)}}

View File

@ -1,4 +1,4 @@
<ChatUserAvatar @user={{@selection.model}} @showPresence={{true}} /> <Chat::UserAvatar @user={{@selection.model}} />
<span class="chat-message-creator__selection-item__username"> <span class="chat-message-creator__selection-item__username">
{{@selection.model.username}} {{@selection.model.username}}

View File

@ -2,10 +2,6 @@
{{#if @message.chatWebhookEvent.emoji}} {{#if @message.chatWebhookEvent.emoji}}
<ChatEmojiAvatar @emoji={{@message.chatWebhookEvent.emoji}} /> <ChatEmojiAvatar @emoji={{@message.chatWebhookEvent.emoji}} />
{{else}} {{else}}
<ChatUserAvatar <Chat::UserAvatar @user={{@message.user}} @avatarSize="medium" />
@showPresence={{true}}
@user={{@message.user}}
@avatarSize="medium"
/>
{{/if}} {{/if}}
</div> </div>

View File

@ -14,7 +14,7 @@
> >
<div class="chat-thread-list-item__header"> <div class="chat-thread-list-item__header">
<div class="chat-thread-list-item__om-user-avatar"> <div class="chat-thread-list-item__om-user-avatar">
<ChatUserAvatar @user={{@thread.originalMessage.user}} /> <Chat::UserAvatar @user={{@thread.originalMessage.user}} />
</div> </div>
<div class="chat-thread-list-item__title overflow-ellipsis"> <div class="chat-thread-list-item__title overflow-ellipsis">
{{replace-emoji this.title}} {{replace-emoji this.title}}

View File

@ -2,7 +2,7 @@
<div class="chat-thread-participants"> <div class="chat-thread-participants">
<div class="chat-thread-participants__avatar-group"> <div class="chat-thread-participants__avatar-group">
{{#each @thread.preview.participantUsers as |user|}} {{#each @thread.preview.participantUsers as |user|}}
<ChatUserAvatar <Chat::UserAvatar
@user={{user}} @user={{user}}
@avatarSize="tiny" @avatarSize="tiny"
@showPresence={{false}} @showPresence={{false}}

View File

@ -0,0 +1,9 @@
<div class="chat-user-avatar {{if this.isOnline 'is-online'}}">
<div
role="button"
class="chat-user-avatar__container clickable"
data-user-card={{@user.username}}
>
{{avatar @user imageSize=this.avatarSize}}
</div>
</div>

View File

@ -8,12 +8,19 @@ export default class ChatUserAvatar extends Component {
return this.args.avatarSize || "tiny"; return this.args.avatarSize || "tiny";
} }
get showPresence() {
return this.args.showPresence ?? true;
}
get isOnline() { get isOnline() {
const users = (this.args.chat || this.chat).presenceChannel?.users; const users = (this.args.chat || this.chat).presenceChannel?.users;
return ( return (
!!users?.findBy("id", this.args.user?.id) || this.showPresence &&
!!users?.findBy("username", this.args.user?.username) !!users?.find(
({ id, username }) =>
this.args.user?.id === id || this.args.user?.username === username
)
); );
} }
} }

View File

@ -186,60 +186,6 @@ html.ios-device.keyboard-visible body #main-outlet .full-page-chat {
} }
} }
.chat-user-avatar {
@include unselectable;
display: flex;
align-items: center;
.chat-message-container:not(.has-reply) & {
width: var(--message-left-width);
flex-shrink: 0;
}
&.is-online {
.chat-user-avatar-container .avatar {
box-shadow: 0px 0px 0px 1px var(--success);
border: 1px solid var(--secondary);
padding: 0;
}
}
.chat-user-avatar-container {
position: relative;
padding: 1px; // for is-online box-shadow effect, preventing cutoff
.avatar {
padding: 1px; // for is-online box-shadow effect, preventing shift
}
.chat-user-presence-flair {
box-sizing: border-box;
position: absolute;
background-color: var(--success);
border: 1px solid var(--secondary);
border-radius: 50%;
.chat-message & {
width: 10px;
height: 10px;
right: 0px;
bottom: 0px;
}
.chat-channel-title & {
width: 8px;
height: 8px;
right: -1px;
bottom: -1px;
}
}
}
.chat-channel-title & {
width: auto;
}
}
.topic-title-chat-icon { .topic-title-chat-icon {
display: inline-block; display: inline-block;
* { * {

View File

@ -121,7 +121,7 @@
} }
} }
.chat-message-avatar .chat-user-avatar .chat-user-avatar-container .avatar, .chat-message-avatar .chat-user-avatar .chat-user-avatar__container .avatar,
.chat-emoji-avatar .chat-emoji-avatar-container { .chat-emoji-avatar .chat-emoji-avatar-container {
width: 28px; width: 28px;
height: 28px; height: 28px;

View File

@ -11,7 +11,7 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
.chat-user-avatar-container { .chat-user-avatar__container {
padding: 0; padding: 0;
} }
@ -32,6 +32,7 @@
&__avatar-group { &__avatar-group {
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
.chat-user-avatar { .chat-user-avatar {
&:not(:last-child) { &:not(:last-child) {
margin-right: -10px; margin-right: -10px;

View File

@ -0,0 +1,53 @@
.chat-user-avatar {
@include unselectable;
display: flex;
align-items: center;
.chat-message-container:not(.has-reply) & {
width: var(--message-left-width);
flex-shrink: 0;
}
&.is-online {
.chat-user-avatar__container .avatar {
box-shadow: 0px 0px 0px 1px var(--success);
border: 1px solid var(--secondary);
padding: 0;
}
}
&__container {
position: relative;
padding: 1px; // for is-online box-shadow effect, preventing cutoff
.avatar {
padding: 1px; // for is-online box-shadow effect, preventing shift
}
.chat-user-presence-flair {
box-sizing: border-box;
position: absolute;
background-color: var(--success);
border: 1px solid var(--secondary);
border-radius: 50%;
.chat-message & {
width: 10px;
height: 10px;
right: 0px;
bottom: 0px;
}
.chat-channel-title & {
width: 8px;
height: 8px;
right: -1px;
bottom: -1px;
}
}
}
.chat-channel-title & {
width: auto;
}
}

View File

@ -59,3 +59,4 @@
@import "chat-message-error"; @import "chat-message-error";
@import "chat-new-message-modal"; @import "chat-new-message-modal";
@import "chat-message-creator"; @import "chat-message-creator";
@import "chat-user-avatar";

View File

@ -33,7 +33,7 @@ module PageObjects
def has_participant?(user) def has_participant?(user)
find(@context).has_css?( find(@context).has_css?(
".chat-thread-participants__avatar-group .chat-user-avatar .chat-user-avatar-container[data-user-card=\"#{user.username}\"] img", ".chat-thread-participants__avatar-group .chat-user-avatar .chat-user-avatar__container[data-user-card=\"#{user.username}\"] img",
) )
end end

View File

@ -32,7 +32,7 @@ module PageObjects
end end
def avatar_selector(user) def avatar_selector(user)
".chat-thread-list-item__om-user-avatar .chat-user-avatar .chat-user-avatar-container[data-user-card=\"#{user.username}\"] img" ".chat-thread-list-item__om-user-avatar .chat-user-avatar .chat-user-avatar__container[data-user-card=\"#{user.username}\"] img"
end end
def last_reply_datetime_selector(last_reply) def last_reply_datetime_selector(last_reply)

View File

@ -71,7 +71,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
const user = this.channel.chatable.users[0]; const user = this.channel.chatable.users[0];
assert.true( assert.true(
exists(`.chat-user-avatar-container .avatar[title="${user.username}"]`) exists(`.chat-user-avatar__container .avatar[title="${user.username}"]`)
); );
assert.strictEqual( assert.strictEqual(

View File

@ -1,50 +1,55 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
const user = { function containerSelector(user, options = {}) {
id: 1, let onlineSelector = ":not(.is-online)";
username: "markvanlan", if (options.online) {
name: null, onlineSelector = ".is-online";
avatar_template: "/letter_avatar_proxy/v4/letter/m/48db29/{size}.png", }
};
module("Discourse Chat | Component | chat-user-avatar", function (hooks) { return `.chat-user-avatar${onlineSelector} .chat-user-avatar__container[data-user-card=${user.username}] .avatar[title=${user.username}]`;
}
module("Discourse Chat | Component | <Chat::UserAvatar />", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("user is not online", async function (assert) { test("user is not online", async function (assert) {
this.set("user", user); this.user = fabricators.user();
this.set("chat", { presenceChannel: { users: [] } }); this.chat = { presenceChannel: { users: [] } };
await render( await render(
hbs`<ChatUserAvatar @chat={{this.chat}} @user={{this.user}} />` hbs`<Chat::UserAvatar @chat={{this.chat}} @user={{this.user}} />`
); );
assert.true( assert.dom(containerSelector(this.user, { online: false })).exists();
exists(
`.chat-user-avatar .chat-user-avatar-container[data-user-card=${user.username}] .avatar[title=${user.username}]`
)
);
assert.false(exists(".chat-user-avatar.is-online"));
}); });
test("user is online", async function (assert) { test("user is online", async function (assert) {
this.set("user", user); this.user = fabricators.user();
this.set("chat", { this.chat = {
presenceChannel: { users: [{ id: user.id }] }, presenceChannel: { users: [{ id: this.user.id }] },
}); };
await render( await render(
hbs`<ChatUserAvatar @showPresence={{true}} @chat={{this.chat}} @user={{this.user}} />` hbs`<Chat::UserAvatar @chat={{this.chat}} @user={{this.user}} />`
); );
assert.true( assert.dom(containerSelector(this.user, { online: true })).exists();
exists( });
`.chat-user-avatar .chat-user-avatar-container[data-user-card=${user.username}] .avatar[title=${user.username}]`
) test("showPresence=false", async function (assert) {
this.user = fabricators.user();
this.chat = {
presenceChannel: { users: [{ id: this.user.id }] },
};
await render(
hbs`<Chat::UserAvatar @showPresence={{false}} @chat={{this.chat}} @user={{this.user}} />`
); );
assert.true(exists(".chat-user-avatar.is-online"));
assert.dom(containerSelector(this.user, { online: false })).exists();
}); });
}); });