PERF: applies optimisations on chat-live pane (#20532)
- group writes when computing separators positions - shows skeleton only on initial load - forces date separator to be pinned when first message to prevent a pinned - not pinned - pinned sequence when loading more in past - relies on `message.visible` property instead of checking `isElementInViewport` - attempts to load next/prev messages earlier - do not scroll to on fetch more - hides `last visit` text while pinned
This commit is contained in:
parent
d28390054e
commit
b5e736504a
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::ChatController < Chat::ChatBaseController
|
class Chat::ChatController < Chat::ChatBaseController
|
||||||
PAST_MESSAGE_LIMIT = 20
|
PAST_MESSAGE_LIMIT = 40
|
||||||
FUTURE_MESSAGE_LIMIT = 40
|
FUTURE_MESSAGE_LIMIT = 40
|
||||||
PAST = "past"
|
PAST = "past"
|
||||||
FUTURE = "future"
|
FUTURE = "future"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"chat-live-pane"
|
"chat-live-pane"
|
||||||
(if this.loading "loading")
|
(if this.loading "loading")
|
||||||
(if this.sendingLoading "sending-loading")
|
(if this.sendingLoading "sending-loading")
|
||||||
|
(unless this.loadedOnce "not-loaded-once")
|
||||||
}}
|
}}
|
||||||
{{did-insert this.setupListeners}}
|
{{did-insert this.setupListeners}}
|
||||||
{{will-destroy this.teardownListeners}}
|
{{will-destroy this.teardownListeners}}
|
||||||
|
@ -37,16 +38,18 @@
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="chat-messages-scroll chat-messages-container">
|
<div
|
||||||
|
class="chat-messages-scroll chat-messages-container"
|
||||||
|
{{chat/on-throttled-scroll this.computeScrollState (hash delay=50)}}
|
||||||
|
{{chat/on-throttled-scroll this.resetIdle (hash delay=500)}}
|
||||||
|
>
|
||||||
<div class="chat-message-actions-desktop-anchor"></div>
|
<div class="chat-message-actions-desktop-anchor"></div>
|
||||||
<div class="chat-messages-container">
|
<div
|
||||||
|
class="chat-messages-container"
|
||||||
|
{{chat/did-mutate-childlist this.computeDatesSeparators}}
|
||||||
|
>
|
||||||
|
|
||||||
{{#if this.loadingMorePast}}
|
{{#if this.loadedOnce}}
|
||||||
<ChatSkeleton
|
|
||||||
@onInsert={{this.onDidInsertSkeleton}}
|
|
||||||
@onDestroy={{this.onDestroySkeleton}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#each @channel.messages key="id" as |message|}}
|
{{#each @channel.messages key="id" as |message|}}
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
|
@ -65,14 +68,11 @@
|
||||||
@resendStagedMessage={{this.resendStagedMessage}}
|
@resendStagedMessage={{this.resendStagedMessage}}
|
||||||
@didShowMessage={{this.didShowMessage}}
|
@didShowMessage={{this.didShowMessage}}
|
||||||
@didHideMessage={{this.didHideMessage}}
|
@didHideMessage={{this.didHideMessage}}
|
||||||
|
@forceRendering={{this.forceRendering}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
{{#if (or this.loadingMoreFuture)}}
|
<ChatSkeleton />
|
||||||
<ChatSkeleton
|
|
||||||
@onInsert={{this.onDidInsertSkeleton}}
|
|
||||||
@onDestroy={{this.onDestroySkeleton}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
<ChatScrollToBottomArrow
|
<ChatScrollToBottomArrow
|
||||||
@scrollToBottom={{this.scrollToBottom}}
|
@scrollToBottom={{this.scrollToBottom}}
|
||||||
@hasNewMessages={{this.hasNewMessages}}
|
@hasNewMessages={{this.hasNewMessages}}
|
||||||
@isAlmostDocked={{this.isAlmostDocked}}
|
@show={{or this.needsArrow @channel.canLoadMoreFuture}}
|
||||||
@channel={{@channel}}
|
@channel={{@channel}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { capitalize } from "@ember/string";
|
import { capitalize } from "@ember/string";
|
||||||
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
|
||||||
import { cloneJSON } from "discourse-common/lib/object";
|
import { cloneJSON } from "discourse-common/lib/object";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
|
import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
|
||||||
|
@ -9,7 +8,7 @@ import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import EmberObject, { action } from "@ember/object";
|
import EmberObject, { action } from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { cancel, next, schedule, throttle } from "@ember/runloop";
|
import { cancel, schedule } from "@ember/runloop";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
|
@ -19,14 +18,10 @@ import {
|
||||||
removeOnPresenceChange,
|
removeOnPresenceChange,
|
||||||
} from "discourse/lib/user-presence";
|
} from "discourse/lib/user-presence";
|
||||||
import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
|
import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
|
|
||||||
const STICKY_SCROLL_LENIENCE = 100;
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
const SCROLL_HANDLER_THROTTLE_MS = isTesting() ? 0 : 150;
|
|
||||||
const FETCH_MORE_MESSAGES_THROTTLE_MS = isTesting() ? 0 : 500;
|
|
||||||
const PAST = "past";
|
const PAST = "past";
|
||||||
const FUTURE = "future";
|
const FUTURE = "future";
|
||||||
const READ_INTERVAL_MS = 1000;
|
const READ_INTERVAL_MS = 1000;
|
||||||
|
@ -54,14 +49,18 @@ export default class ChatLivePane extends Component {
|
||||||
@tracked includeHeader = true;
|
@tracked includeHeader = true;
|
||||||
@tracked editingMessage = null;
|
@tracked editingMessage = null;
|
||||||
@tracked replyToMsg = null;
|
@tracked replyToMsg = null;
|
||||||
@tracked hasNewMessages = null;
|
@tracked hasNewMessages = false;
|
||||||
@tracked isDocked = true;
|
@tracked needsArrow = false;
|
||||||
@tracked isAlmostDocked = true;
|
|
||||||
@tracked loadedOnce = false;
|
@tracked loadedOnce = false;
|
||||||
|
|
||||||
|
isAtBottom = true;
|
||||||
|
isTowardsBottom = false;
|
||||||
|
isTowardsTop = false;
|
||||||
|
isAtTop = false;
|
||||||
|
|
||||||
_loadedChannelId = null;
|
_loadedChannelId = null;
|
||||||
_scrollerEl = null;
|
_scrollerEl = null;
|
||||||
_previousScrollTop = null;
|
_previousScrollTop = 0;
|
||||||
_lastSelectedMessage = null;
|
_lastSelectedMessage = null;
|
||||||
_mentionWarningsSeen = {};
|
_mentionWarningsSeen = {};
|
||||||
_unreachableGroupMentions = [];
|
_unreachableGroupMentions = [];
|
||||||
|
@ -70,14 +69,8 @@ export default class ChatLivePane extends Component {
|
||||||
@action
|
@action
|
||||||
setupListeners(element) {
|
setupListeners(element) {
|
||||||
this._scrollerEl = element.querySelector(".chat-messages-scroll");
|
this._scrollerEl = element.querySelector(".chat-messages-scroll");
|
||||||
this._scrollerEl.addEventListener("scroll", this.onScrollHandler, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
window.addEventListener("resize", this.onResizeHandler);
|
|
||||||
window.addEventListener("wheel", this.onScrollHandler, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
window.addEventListener("resize", this.onResizeHandler);
|
||||||
document.addEventListener("scroll", this._forceBodyScroll, {
|
document.addEventListener("scroll", this._forceBodyScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
@ -88,12 +81,8 @@ export default class ChatLivePane extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
teardownListeners(element) {
|
teardownListeners() {
|
||||||
element
|
|
||||||
.querySelector(".chat-messages-scroll")
|
|
||||||
?.removeEventListener("scroll", this.onScrollHandler);
|
|
||||||
window.removeEventListener("resize", this.onResizeHandler);
|
window.removeEventListener("resize", this.onResizeHandler);
|
||||||
window.removeEventListener("wheel", this.onScrollHandler);
|
|
||||||
cancel(this.resizeHandler);
|
cancel(this.resizeHandler);
|
||||||
document.removeEventListener("scroll", this._forceBodyScroll);
|
document.removeEventListener("scroll", this._forceBodyScroll);
|
||||||
removeOnPresenceChange(this.onPresenceChangeCallback);
|
removeOnPresenceChange(this.onPresenceChangeCallback);
|
||||||
|
@ -101,6 +90,11 @@ export default class ChatLivePane extends Component {
|
||||||
this.requestedTargetMessageId = null;
|
this.requestedTargetMessageId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
resetIdle() {
|
||||||
|
resetIdle();
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateChannel() {
|
updateChannel() {
|
||||||
// Technically we could keep messages to avoid re-fetching them, but
|
// Technically we could keep messages to avoid re-fetching them, but
|
||||||
|
@ -120,25 +114,20 @@ export default class ChatLivePane extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
loadMessages() {
|
loadMessages() {
|
||||||
this.loadedOnce = false;
|
if (!this.args.channel?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.args.targetMessageId) {
|
if (this.args.targetMessageId) {
|
||||||
this.requestedTargetMessageId = parseInt(this.args.targetMessageId, 10);
|
this.requestedTargetMessageId = parseInt(this.args.targetMessageId, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.args.channel?.id) {
|
|
||||||
if (this.requestedTargetMessageId) {
|
if (this.requestedTargetMessageId) {
|
||||||
this.highlightOrFetchMessage(this.requestedTargetMessageId);
|
this.highlightOrFetchMessage(this.requestedTargetMessageId);
|
||||||
} else {
|
} else {
|
||||||
this.fetchMessages({ fetchFromLastMessage: false });
|
this.fetchMessages({ fetchFromLastMessage: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
onScrollHandler(event) {
|
|
||||||
throttle(this, this.onScroll, event, SCROLL_HANDLER_THROTTLE_MS, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onResizeHandler() {
|
onResizeHandler() {
|
||||||
|
@ -168,7 +157,8 @@ export default class ChatLivePane extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.args.channel?.clearMessages();
|
this.loadedOnce = false;
|
||||||
|
this._previousScrollTop = 0;
|
||||||
this.loadingMorePast = true;
|
this.loadingMorePast = true;
|
||||||
|
|
||||||
const findArgs = { pageSize: PAGE_SIZE };
|
const findArgs = { pageSize: PAGE_SIZE };
|
||||||
|
@ -193,9 +183,9 @@ export default class ChatLivePane extends Component {
|
||||||
this.args.channel,
|
this.args.channel,
|
||||||
results
|
results
|
||||||
);
|
);
|
||||||
this.args.channel.addMessages(messages);
|
|
||||||
|
this.args.channel.messages = messages;
|
||||||
this.args.channel.details = meta;
|
this.args.channel.details = meta;
|
||||||
this.loadedOnce = true;
|
|
||||||
|
|
||||||
if (this.requestedTargetMessageId) {
|
if (this.requestedTargetMessageId) {
|
||||||
this.scrollToMessage(findArgs["targetMessageId"], {
|
this.scrollToMessage(findArgs["targetMessageId"], {
|
||||||
|
@ -206,8 +196,6 @@ export default class ChatLivePane extends Component {
|
||||||
} else if (messages.length) {
|
} else if (messages.length) {
|
||||||
this.scrollToMessage(messages[messages.length - 1].id);
|
this.scrollToMessage(messages[messages.length - 1].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fillPaneAttempt();
|
|
||||||
})
|
})
|
||||||
.catch(this._handleErrors)
|
.catch(this._handleErrors)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -215,24 +203,15 @@ export default class ChatLivePane extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadedOnce = true;
|
||||||
this.requestedTargetMessageId = null;
|
this.requestedTargetMessageId = null;
|
||||||
this.loadingMorePast = false;
|
this.loadingMorePast = false;
|
||||||
|
this.fillPaneAttempt();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
onDestroySkeleton() {
|
|
||||||
this._iOSFix();
|
|
||||||
this._throttleComputeSeparators();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onDidInsertSkeleton() {
|
|
||||||
this._computeSeparators(); // this one is not throttled as we need instant feedback
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_fetchMoreMessages({ direction, scrollTo = true }) {
|
fetchMoreMessages({ direction }) {
|
||||||
const loadingPast = direction === PAST;
|
const loadingPast = direction === PAST;
|
||||||
const loadingMoreKey = `loadingMore${capitalize(direction)}`;
|
const loadingMoreKey = `loadingMore${capitalize(direction)}`;
|
||||||
|
|
||||||
|
@ -272,40 +251,48 @@ export default class ChatLivePane extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prevents an edge case where user clicks bottom arrow
|
||||||
|
// just after scrolling to top
|
||||||
|
if (loadingPast && this.isAtBottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [messages, meta] = this.afterFetchCallback(
|
const [messages, meta] = this.afterFetchCallback(
|
||||||
this.args.channel,
|
this.args.channel,
|
||||||
results
|
results
|
||||||
);
|
);
|
||||||
|
|
||||||
this.args.channel.addMessages(messages);
|
if (!messages?.length) {
|
||||||
this.args.channel.details = meta;
|
|
||||||
|
|
||||||
if (!messages.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollTo) {
|
this.args.channel.addMessages(messages);
|
||||||
if (!loadingPast) {
|
this.args.channel.details = meta;
|
||||||
this.scrollToMessage(messageId, { position: "start" });
|
|
||||||
} else {
|
|
||||||
if (this.site.desktopView) {
|
|
||||||
this.scrollToMessage(messages[messages.length - 1].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fillPaneAttempt();
|
// Edge case for IOS to avoid blank screens
|
||||||
|
// and/or scrolling to bottom losing track of scroll position
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
if (
|
||||||
|
!this._selfDeleted &&
|
||||||
|
!loadingPast &&
|
||||||
|
(this.capabilities.isIOS || !this.isScrolling)
|
||||||
|
) {
|
||||||
|
this.scrollToMessage(messages[0].id, { position: "end" });
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this._handleErrors();
|
this._handleErrors();
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this[loadingMoreKey] = false;
|
this[loadingMoreKey] = false;
|
||||||
|
this.fillPaneAttempt();
|
||||||
|
this.computeDatesSeparators();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@debounce(500, false)
|
||||||
fillPaneAttempt() {
|
fillPaneAttempt() {
|
||||||
next(() => {
|
|
||||||
if (this._selfDeleted) {
|
if (this._selfDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -319,40 +306,14 @@ export default class ChatLivePane extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
const firstMessage = this.args.channel?.messages?.[0];
|
||||||
const firstMessageId = this.args.channel?.messages?.[0]?.id;
|
if (!firstMessage?.visible) {
|
||||||
if (!firstMessageId) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scroller = document.querySelector(".chat-messages-container");
|
this.fetchMoreMessages({
|
||||||
const messageContainer = scroller.querySelector(
|
|
||||||
`.chat-message-container[data-id="${firstMessageId}"]`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!scroller ||
|
|
||||||
!messageContainer ||
|
|
||||||
!isElementInViewport(messageContainer)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._fetchMoreMessagesThrottled({
|
|
||||||
direction: PAST,
|
direction: PAST,
|
||||||
scrollTo: false,
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_fetchMoreMessagesThrottled(params) {
|
|
||||||
throttle(
|
|
||||||
this,
|
|
||||||
this._fetchMoreMessages,
|
|
||||||
params,
|
|
||||||
FETCH_MORE_MESSAGES_THROTTLE_MS
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -360,11 +321,15 @@ export default class ChatLivePane extends Component {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
let foundFirstNew = false;
|
let foundFirstNew = false;
|
||||||
|
|
||||||
results.chat_messages.forEach((messageData) => {
|
results.chat_messages.forEach((messageData, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
messageData.firstOfResults = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentUser.ignored_users) {
|
||||||
// If a message has been hidden it is because the current user is ignoring
|
// If a message has been hidden it is because the current user is ignoring
|
||||||
// the user who sent it, so we want to unconditionally hide it, even if
|
// the user who sent it, so we want to unconditionally hide it, even if
|
||||||
// we are going directly to the target
|
// we are going directly to the target
|
||||||
if (this.currentUser.ignored_users) {
|
|
||||||
messageData.hidden = this.currentUser.ignored_users.includes(
|
messageData.hidden = this.currentUser.ignored_users.includes(
|
||||||
messageData.user.username
|
messageData.user.username
|
||||||
);
|
);
|
||||||
|
@ -447,7 +412,7 @@ export default class ChatLivePane extends Component {
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._iOSFix(() => {
|
this.forceRendering(() => {
|
||||||
messageEl.scrollIntoView({
|
messageEl.scrollIntoView({
|
||||||
block: opts.position ?? "center",
|
block: opts.position ?? "center",
|
||||||
});
|
});
|
||||||
|
@ -459,13 +424,11 @@ export default class ChatLivePane extends Component {
|
||||||
didShowMessage(message) {
|
didShowMessage(message) {
|
||||||
message.visible = true;
|
message.visible = true;
|
||||||
this.updateLastReadMessage(message);
|
this.updateLastReadMessage(message);
|
||||||
this._throttleComputeSeparators();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
didHideMessage(message) {
|
didHideMessage(message) {
|
||||||
message.visible = false;
|
message.visible = false;
|
||||||
this._throttleComputeSeparators();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@debounce(READ_INTERVAL_MS)
|
@debounce(READ_INTERVAL_MS)
|
||||||
|
@ -490,67 +453,55 @@ export default class ChatLivePane extends Component {
|
||||||
if (this.args.channel.canLoadMoreFuture) {
|
if (this.args.channel.canLoadMoreFuture) {
|
||||||
this._fetchAndScrollToLatest();
|
this._fetchAndScrollToLatest();
|
||||||
} else {
|
} else {
|
||||||
if (this._scrollerEl) {
|
this.scrollToMessage(
|
||||||
// Trigger a tiny scrollTop change so Safari scrollbar is placed at bottom.
|
this.args.channel.messages[this.args.channel.messages.length - 1].id
|
||||||
// Setting to just 0 doesn't work (it's at 0 by default, so there is no change)
|
);
|
||||||
// Very hacky, but no way to get around this Safari bug
|
|
||||||
this._scrollerEl.scrollTop = -1;
|
|
||||||
|
|
||||||
this._iOSFix(() => {
|
|
||||||
this._scrollerEl.scrollTop = 0;
|
|
||||||
this.hasNewMessages = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll() {
|
@action
|
||||||
|
computeScrollState(event) {
|
||||||
if (this._selfDeleted) {
|
if (this._selfDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetIdle();
|
cancel(this.onScrollEndedHandler);
|
||||||
|
|
||||||
if (this.loading || this.loadingMorePast || this.loadingMoreFuture) {
|
this.isScrolling = true;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollPosition = Math.abs(this._scrollerEl.scrollTop);
|
const scrollPosition = Math.abs(event.target.scrollTop);
|
||||||
const total = this._scrollerEl.scrollHeight - this._scrollerEl.clientHeight;
|
const total = event.target.scrollHeight - event.target.clientHeight;
|
||||||
|
const ratio = (scrollPosition / total) * 100;
|
||||||
|
this.isTowardsTop = ratio < 99 && ratio >= 34;
|
||||||
|
this.isTowardsBottom = ratio > 1 && ratio <= 4;
|
||||||
|
this.isAtBottom = ratio <= 1;
|
||||||
|
this.isAtTop = ratio >= 99;
|
||||||
|
this.needsArrow = ratio >= 5;
|
||||||
|
|
||||||
this.isAlmostDocked = scrollPosition / this._scrollerEl.offsetHeight < 0.67;
|
if (this._previousScrollTop - scrollPosition <= 0) {
|
||||||
this.isDocked = scrollPosition <= 1;
|
if (this.isTowardsTop || this.isAtTop) {
|
||||||
|
this.fetchMoreMessages({ direction: PAST });
|
||||||
if (
|
|
||||||
this._previousScrollTop - this._scrollerEl.scrollTop >
|
|
||||||
this._previousScrollTop
|
|
||||||
) {
|
|
||||||
const atTop = this._isBetween(
|
|
||||||
scrollPosition,
|
|
||||||
total - STICKY_SCROLL_LENIENCE,
|
|
||||||
total + STICKY_SCROLL_LENIENCE
|
|
||||||
);
|
|
||||||
|
|
||||||
if (atTop) {
|
|
||||||
this._fetchMoreMessagesThrottled({ direction: PAST });
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const atBottom = this._isBetween(
|
if (this.isTowardsBottom || this.isAtBottom) {
|
||||||
scrollPosition,
|
this.fetchMoreMessages({ direction: FUTURE });
|
||||||
0 + STICKY_SCROLL_LENIENCE,
|
}
|
||||||
0 - STICKY_SCROLL_LENIENCE
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (atBottom) {
|
this._previousScrollTop = scrollPosition;
|
||||||
|
this.onScrollEndedHandler = discourseLater(this, this.onScrollEnded, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onScrollEnded() {
|
||||||
|
this.isScrolling = false;
|
||||||
|
|
||||||
|
if (this.isAtBottom) {
|
||||||
this.hasNewMessages = false;
|
this.hasNewMessages = false;
|
||||||
this._fetchMoreMessagesThrottled({ direction: FUTURE });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._previousScrollTop = this._scrollerEl.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isBetween(target, a, b) {
|
_isBetween(target, a, b) {
|
||||||
const min = Math.min.apply(Math, [a, b]);
|
const min = Math.min.apply(Math, [a, b]);
|
||||||
const max = Math.max.apply(Math, [a, b]);
|
const max = Math.max.apply(Math, [a, b]);
|
||||||
|
@ -640,7 +591,7 @@ export default class ChatLivePane extends Component {
|
||||||
if (this.args.channel.canLoadMoreFuture) {
|
if (this.args.channel.canLoadMoreFuture) {
|
||||||
// If we can load more messages, we just notice the user of new messages
|
// If we can load more messages, we just notice the user of new messages
|
||||||
this.hasNewMessages = true;
|
this.hasNewMessages = true;
|
||||||
} else if (this._scrollerEl.scrollTop <= 1) {
|
} else if (this.isAtBottom || this.isTowardsBottom) {
|
||||||
// If we are at the bottom, we append the message and scroll to it
|
// If we are at the bottom, we append the message and scroll to it
|
||||||
const message = ChatMessage.create(this.args.channel, data.chat_message);
|
const message = ChatMessage.create(this.args.channel, data.chat_message);
|
||||||
this.args.channel.addMessages([message]);
|
this.args.channel.addMessages([message]);
|
||||||
|
@ -1107,6 +1058,10 @@ export default class ChatLivePane extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isScrolling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message?.staged) {
|
if (message?.staged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1140,20 +1095,6 @@ export default class ChatLivePane extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._onHoverMessageDebouncedHandler = discourseDebounce(
|
|
||||||
this,
|
|
||||||
this.debouncedOnHoverMessage,
|
|
||||||
message,
|
|
||||||
250
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
debouncedOnHoverMessage(message) {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hoveredMessageId =
|
this.hoveredMessageId =
|
||||||
message?.id && message.id !== this.hoveredMessageId ? message.id : null;
|
message?.id && message.id !== this.hoveredMessageId ? message.id : null;
|
||||||
}
|
}
|
||||||
|
@ -1216,6 +1157,7 @@ export default class ChatLivePane extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchAndScrollToLatest() {
|
_fetchAndScrollToLatest() {
|
||||||
|
this.loadedOnce = false;
|
||||||
return this.fetchMessages({
|
return this.fetchMessages({
|
||||||
fetchFromLastMessage: true,
|
fetchFromLastMessage: true,
|
||||||
});
|
});
|
||||||
|
@ -1235,12 +1177,14 @@ export default class ChatLivePane extends Component {
|
||||||
// since -webkit-overflow-scrolling: touch can't be used anymore to disable momentum scrolling
|
// since -webkit-overflow-scrolling: touch can't be used anymore to disable momentum scrolling
|
||||||
// we now use this hack to disable it
|
// we now use this hack to disable it
|
||||||
@bind
|
@bind
|
||||||
_iOSFix(callback) {
|
forceRendering(callback) {
|
||||||
|
schedule("afterRender", () => {
|
||||||
if (!this._scrollerEl) {
|
if (!this._scrollerEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.capabilities.isIOS) {
|
if (this.capabilities.isIOS) {
|
||||||
|
this._scrollerEl.style.transform = "translateZ(0)";
|
||||||
this._scrollerEl.style.overflow = "hidden";
|
this._scrollerEl.style.overflow = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1253,8 +1197,11 @@ export default class ChatLivePane extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._scrollerEl.style.overflow = "auto";
|
this._scrollerEl.style.overflow = "auto";
|
||||||
}, 25);
|
this._scrollerEl.style.transform = "unset";
|
||||||
|
this.computeDatesSeparators();
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -1300,30 +1247,27 @@ export default class ChatLivePane extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_throttleComputeSeparators() {
|
@action
|
||||||
throttle(this, this._computeSeparators, 32, false);
|
computeDatesSeparators() {
|
||||||
}
|
|
||||||
|
|
||||||
_computeSeparators() {
|
|
||||||
next(() => {
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
const dates = this._scrollerEl.querySelectorAll(
|
const dates = [
|
||||||
".chat-message-separator-date"
|
...this._scrollerEl.querySelectorAll(".chat-message-separator-date"),
|
||||||
);
|
].reverse();
|
||||||
const scrollHeight = document.querySelector(
|
const scrollHeight = this._scrollerEl.scrollHeight;
|
||||||
".chat-messages-scroll"
|
|
||||||
).scrollHeight;
|
dates
|
||||||
const reversedDates = [...dates].reverse();
|
.map((date, index) => {
|
||||||
// TODO (joffrey): optimize this code to trigger less layout computation
|
const item = { bottom: "0px", date };
|
||||||
reversedDates.forEach((date, index) => {
|
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
date.style.bottom =
|
item.bottom = scrollHeight - dates[index - 1].offsetTop + "px";
|
||||||
scrollHeight - reversedDates[index - 1].offsetTop + "px";
|
|
||||||
} else {
|
|
||||||
date.style.bottom = 0;
|
|
||||||
}
|
}
|
||||||
date.style.top = date.nextElementSibling.offsetTop + "px";
|
item.top = date.nextElementSibling.offsetTop + "px";
|
||||||
});
|
return item;
|
||||||
|
})
|
||||||
|
// group all writes at the end
|
||||||
|
.forEach((item) => {
|
||||||
|
item.date.style.bottom = item.bottom;
|
||||||
|
item.date.style.top = item.top;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,30 @@
|
||||||
{{#if this.hasUploads}}
|
{{#if this.hasUploads}}
|
||||||
{{html-safe @cooked}}
|
{{html-safe @cooked}}
|
||||||
|
|
||||||
<Collapser @header={{this.uploadsHeader}}>
|
<Collapser
|
||||||
|
@header={{this.uploadsHeader}}
|
||||||
|
@onToggle={{@onToggleCollapse}}
|
||||||
|
as |collapsed|
|
||||||
|
>
|
||||||
|
{{#unless collapsed}}
|
||||||
<div class="chat-uploads">
|
<div class="chat-uploads">
|
||||||
{{#each @uploads as |upload|}}
|
{{#each @uploads as |upload|}}
|
||||||
<ChatUpload @upload={{upload}} />
|
<ChatUpload @upload={{upload}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
{{/unless}}
|
||||||
</Collapser>
|
</Collapser>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#each this.cookedBodies as |cooked|}}
|
{{#each this.cookedBodies as |cooked|}}
|
||||||
{{#if cooked.needsCollapser}}
|
{{#if cooked.needsCollapser}}
|
||||||
<Collapser @header={{cooked.header}}>
|
<Collapser
|
||||||
|
@header={{cooked.header}}
|
||||||
|
@onToggle={{@onToggleCollapse}}
|
||||||
|
as |collapsed|
|
||||||
|
>
|
||||||
|
{{#unless collapsed}}
|
||||||
{{cooked.body}}
|
{{cooked.body}}
|
||||||
|
{{/unless}}
|
||||||
</Collapser>
|
</Collapser>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{cooked.body}}
|
{{cooked.body}}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div
|
<div
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message-separator-date"
|
"chat-message-separator-date"
|
||||||
(if @message.newest "last-visit")
|
(if @message.newest "with-last-visit")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -10,11 +10,13 @@
|
||||||
{{chat/track-message-separator-date}}
|
{{chat/track-message-separator-date}}
|
||||||
>
|
>
|
||||||
<span class="chat-message-separator__text">
|
<span class="chat-message-separator__text">
|
||||||
{{@message.firstMessageOfTheDayAt}}
|
<span>{{@message.firstMessageOfTheDayAt}}</span>
|
||||||
|
|
||||||
{{#if @message.newest}}
|
{{#if @message.newest}}
|
||||||
|
<span class="chat-message-separator__last-visit">
|
||||||
-
|
-
|
||||||
{{i18n "chat.last_visit"}}
|
{{i18n "chat.last_visit"}}
|
||||||
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<div class="chat-message-text">
|
<div class="chat-message-text">
|
||||||
{{#if this.isCollapsible}}
|
{{#if this.isCollapsible}}
|
||||||
<ChatMessageCollapser @cooked={{@cooked}} @uploads={{@uploads}} />
|
<ChatMessageCollapser
|
||||||
|
@cooked={{@cooked}}
|
||||||
|
@uploads={{@uploads}}
|
||||||
|
@onToggleCollapse={{@onToggleCollapse}}
|
||||||
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{html-safe @cooked}}
|
{{html-safe @cooked}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
@cooked={{@message.cooked}}
|
@cooked={{@message.cooked}}
|
||||||
@uploads={{@message.uploads}}
|
@uploads={{@message.uploads}}
|
||||||
@edited={{@message.edited}}
|
@edited={{@message.edited}}
|
||||||
|
@onToggleCollapse={{fn @forceRendering (noop)}}
|
||||||
>
|
>
|
||||||
{{#if @message.reactions.length}}
|
{{#if @message.reactions.length}}
|
||||||
<div class="chat-message-reaction-list">
|
<div class="chat-message-reaction-list">
|
||||||
|
|
|
@ -270,16 +270,27 @@ export default class ChatMessage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get hideUserInfo() {
|
get hideUserInfo() {
|
||||||
|
const message = this.args.message;
|
||||||
|
const previousMessage = message?.previousMessage;
|
||||||
|
|
||||||
|
if (!previousMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a micro optimization to avoid layout changes when we load more messages
|
||||||
|
if (message?.firstOfResults) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!this.args.message?.chatWebhookEvent &&
|
!message?.chatWebhookEvent &&
|
||||||
!this.args.message?.inReplyTo &&
|
(!message?.inReplyTo ||
|
||||||
!this.args.message?.previousMessage?.deletedAt &&
|
message?.inReplyTo?.user?.id !== message?.user?.id) &&
|
||||||
|
!message?.previousMessage?.deletedAt &&
|
||||||
Math.abs(
|
Math.abs(
|
||||||
new Date(this.args.message?.createdAt) -
|
new Date(message?.createdAt) - new Date(previousMessage?.createdAt)
|
||||||
new Date(this.args.message?.createdAt)
|
|
||||||
) < 300000 && // If the time between messages is over 5 minutes, break.
|
) < 300000 && // If the time between messages is over 5 minutes, break.
|
||||||
this.args.message?.user?.id ===
|
message?.user?.id === message?.previousMessage?.user?.id
|
||||||
this.args.message?.previousMessage?.user?.id
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,6 +517,8 @@ export default class ChatMessage extends Component {
|
||||||
this.currentUser.id
|
this.currentUser.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.args.forceRendering?.();
|
||||||
|
|
||||||
return ajax(
|
return ajax(
|
||||||
`/chat/${this.args.message.channelId}/react/${this.args.message.id}`,
|
`/chat/${this.args.message.channelId}/react/${this.args.message.id}`,
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,10 +3,7 @@
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"btn-flat"
|
"btn-flat"
|
||||||
"chat-scroll-to-bottom"
|
"chat-scroll-to-bottom"
|
||||||
(if
|
(if (or @show @hasNewMessages) "visible")
|
||||||
(or (not @isAlmostDocked) @hasNewMessages @channel.canLoadMoreFuture)
|
|
||||||
"visible"
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
@action={{@scrollToBottom}}
|
@action={{@scrollToBottom}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
<div
|
<div class="chat-skeleton -animation">
|
||||||
class="chat-skeleton -animation"
|
|
||||||
{{did-insert @onInsert}}
|
|
||||||
{{will-destroy @onDestroy}}
|
|
||||||
>
|
|
||||||
{{#each this.placeholders as |placeholder|}}
|
{{#each this.placeholders as |placeholder|}}
|
||||||
<div class="chat-skeleton__body">
|
<div class="chat-skeleton__body">
|
||||||
<div class="chat-skeleton__message">
|
<div class="chat-skeleton__message">
|
||||||
|
|
|
@ -16,6 +16,11 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={{if this.collapsed "hidden" ""}}>
|
<div
|
||||||
{{yield}}
|
class={{concat-class
|
||||||
|
"chat-message-collapser-body"
|
||||||
|
(if this.collapsed "hidden")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{yield this.collapsed}}
|
||||||
</div>
|
</div>
|
|
@ -6,14 +6,17 @@ export default Component.extend({
|
||||||
|
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
header: null,
|
header: null,
|
||||||
|
onToggle: null,
|
||||||
|
|
||||||
@action
|
@action
|
||||||
open() {
|
open() {
|
||||||
this.set("collapsed", false);
|
this.set("collapsed", false);
|
||||||
|
this.onToggle?.(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
close() {
|
close() {
|
||||||
this.set("collapsed", true);
|
this.set("collapsed", true);
|
||||||
|
this.onToggle?.(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,11 +47,13 @@ export default class ChatMessage {
|
||||||
@tracked availableFlags;
|
@tracked availableFlags;
|
||||||
@tracked newest = false;
|
@tracked newest = false;
|
||||||
@tracked highlighted = false;
|
@tracked highlighted = false;
|
||||||
|
@tracked firstOfResults = false;
|
||||||
|
|
||||||
constructor(channel, args = {}) {
|
constructor(channel, args = {}) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
this.id = args.id;
|
this.id = args.id;
|
||||||
this.newest = args.newest;
|
this.newest = args.newest;
|
||||||
|
this.firstOfResults = args.firstOfResults;
|
||||||
this.staged = args.staged;
|
this.staged = args.staged;
|
||||||
this.edited = args.edited;
|
this.edited = args.edited;
|
||||||
this.availableFlags = args.available_flags;
|
this.availableFlags = args.available_flags;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
|
||||||
|
export default class ChatDidMutateChildlist extends Modifier {
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(element, [callback]) {
|
||||||
|
this.mutationObserver = new MutationObserver(() => {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mutationObserver.observe(element, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.mutationObserver?.disconnect();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
import { cancel, throttle } from "@ember/runloop";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
export default class ChatOnThrottledScroll extends Modifier {
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(element, [callback, options]) {
|
||||||
|
this.element = element;
|
||||||
|
this.callback = callback;
|
||||||
|
this.options = options;
|
||||||
|
this.element.addEventListener("scroll", this.throttledCallback, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
throttledCallback(event) {
|
||||||
|
this.throttledHandler = throttle(
|
||||||
|
this,
|
||||||
|
this.callback,
|
||||||
|
event,
|
||||||
|
this.options.delay ?? 100,
|
||||||
|
this.options.immediate ?? false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cancel(this.throttledHandler);
|
||||||
|
this.element.removeEventListener("scroll", this.throttledCallback);
|
||||||
|
}
|
||||||
|
}
|
|
@ -243,7 +243,7 @@ export default class ChatApi extends Service {
|
||||||
* @param {integer} data.pageSize - Max number of messages to fetch.
|
* @param {integer} data.pageSize - Max number of messages to fetch.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async messages(channelId, data = {}) {
|
messages(channelId, data = {}) {
|
||||||
let path;
|
let path;
|
||||||
const args = {};
|
const args = {};
|
||||||
|
|
||||||
|
|
|
@ -134,6 +134,10 @@ export default class ChatChannelsManager extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
#cache(channel) {
|
#cache(channel) {
|
||||||
|
if (!channel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._cached[channel.id] = channel;
|
this._cached[channel.id] = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&.last-visit {
|
&.with-last-visit {
|
||||||
& + .chat-message-separator__line-container {
|
& + .chat-message-separator__line-container {
|
||||||
.chat-message-separator__line {
|
.chat-message-separator__line {
|
||||||
border-color: var(--danger-medium);
|
border-color: var(--danger-medium);
|
||||||
|
@ -57,11 +57,16 @@
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
|
|
||||||
&.is-pinned {
|
&.is-pinned,
|
||||||
|
&.is-force-pinned {
|
||||||
.chat-message-separator__text {
|
.chat-message-separator__text {
|
||||||
border: 1px solid var(--primary-medium);
|
border: 1px solid var(--primary-medium);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-message-separator__last-visit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,9 +145,13 @@ $float-height: 530px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
will-change: transform;
|
||||||
|
transform: translateZ(0);
|
||||||
|
|
||||||
.chat-message-container {
|
.chat-message-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
will-change: transform;
|
||||||
|
transform: translateZ(0);
|
||||||
|
|
||||||
&.selecting-messages {
|
&.selecting-messages {
|
||||||
grid-template-columns: 1.5em 1fr;
|
grid-template-columns: 1.5em 1fr;
|
||||||
|
@ -332,7 +336,7 @@ $float-height: 530px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
bottom: -75px;
|
bottom: -25px;
|
||||||
background: none;
|
background: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.25s ease, transform 0.5s ease;
|
transition: opacity 0.25s ease, transform 0.5s ease;
|
||||||
|
@ -350,7 +354,7 @@ $float-height: 530px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.visible {
|
&.visible {
|
||||||
transform: translateY(-75px) scale(1);
|
transform: translateY(-32px) scale(1);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,6 @@ RSpec.describe "Bookmark message", type: :system, js: true do
|
||||||
context "when mobile", mobile: true do
|
context "when mobile", mobile: true do
|
||||||
it "allows to bookmark a message" do
|
it "allows to bookmark a message" do
|
||||||
chat.visit_channel(category_channel_1)
|
chat.visit_channel(category_channel_1)
|
||||||
expect(channel).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
i = 0.5
|
i = 0.5
|
||||||
try_until_success(timeout: 20) do
|
try_until_success(timeout: 20) do
|
||||||
|
|
|
@ -35,7 +35,6 @@ RSpec.describe "Chat channel", type: :system, js: true do
|
||||||
|
|
||||||
it "allows to edit this message once persisted" do
|
it "allows to edit this message once persisted" do
|
||||||
chat.visit_channel(channel_1)
|
chat.visit_channel(channel_1)
|
||||||
expect(channel).to have_no_loading_skeleton
|
|
||||||
channel.send_message("aaaaaaaaaaaaaaaaaaaa")
|
channel.send_message("aaaaaaaaaaaaaaaaaaaa")
|
||||||
expect(page).to have_no_css(".chat-message-staged")
|
expect(page).to have_no_css(".chat-message-staged")
|
||||||
last_message = find(".chat-message-container:last-child")
|
last_message = find(".chat-message-container:last-child")
|
||||||
|
|
|
@ -16,7 +16,6 @@ RSpec.describe "Deleted message", type: :system, js: true do
|
||||||
context "when deleting a message" do
|
context "when deleting a message" do
|
||||||
it "shows as deleted" do
|
it "shows as deleted" do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
expect(channel_page).to have_no_loading_skeleton
|
|
||||||
channel_page.send_message("aaaaaaaaaaaaaaaaaaaa")
|
channel_page.send_message("aaaaaaaaaaaaaaaaaaaa")
|
||||||
expect(page).to have_no_css(".chat-message-staged")
|
expect(page).to have_no_css(".chat-message-staged")
|
||||||
last_message = find(".chat-message-container:last-child")
|
last_message = find(".chat-message-container:last-child")
|
||||||
|
|
|
@ -25,7 +25,6 @@ describe "Using #hashtag autocompletion to search for and lookup channels",
|
||||||
|
|
||||||
it "searches for channels, categories, and tags with # and prioritises channels in the results" do
|
it "searches for channels, categories, and tags with # and prioritises channels in the results" do
|
||||||
chat_page.visit_channel(channel1)
|
chat_page.visit_channel(channel1)
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
chat_channel_page.type_in_composer("this is #ra")
|
chat_channel_page.type_in_composer("this is #ra")
|
||||||
expect(page).to have_css(
|
expect(page).to have_css(
|
||||||
".hashtag-autocomplete .hashtag-autocomplete__option .hashtag-autocomplete__link",
|
".hashtag-autocomplete .hashtag-autocomplete__option .hashtag-autocomplete__link",
|
||||||
|
@ -53,7 +52,6 @@ describe "Using #hashtag autocompletion to search for and lookup channels",
|
||||||
|
|
||||||
it "cooks the hashtags for channels, categories, and tags serverside when the chat message is saved to the database" do
|
it "cooks the hashtags for channels, categories, and tags serverside when the chat message is saved to the database" do
|
||||||
chat_page.visit_channel(channel1)
|
chat_page.visit_channel(channel1)
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
chat_channel_page.type_in_composer(
|
chat_channel_page.type_in_composer(
|
||||||
"this is #random and this is #raspberry-beret and this is #razed which is cool",
|
"this is #random and this is #raspberry-beret and this is #razed which is cool",
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,6 @@ RSpec.describe "Mentions warnings", type: :system, js: true do
|
||||||
|
|
||||||
it "displays a warning" do
|
it "displays a warning" do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
chat_channel_page.type_in_composer("@#{admin_mentionable_group.name} ")
|
chat_channel_page.type_in_composer("@#{admin_mentionable_group.name} ")
|
||||||
|
|
||||||
expect(page).to have_css(".chat-mention-warnings")
|
expect(page).to have_css(".chat-mention-warnings")
|
||||||
|
@ -46,7 +45,6 @@ RSpec.describe "Mentions warnings", type: :system, js: true do
|
||||||
|
|
||||||
it "displays a warning" do
|
it "displays a warning" do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
chat_channel_page.type_in_composer("@#{publicly_mentionable_group.name} ")
|
chat_channel_page.type_in_composer("@#{publicly_mentionable_group.name} ")
|
||||||
|
|
||||||
expect(page).to have_css(".chat-mention-warnings")
|
expect(page).to have_css(".chat-mention-warnings")
|
||||||
|
@ -61,7 +59,6 @@ RSpec.describe "Mentions warnings", type: :system, js: true do
|
||||||
|
|
||||||
it "displays a warning" do
|
it "displays a warning" do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
chat_channel_page.type_in_composer(
|
chat_channel_page.type_in_composer(
|
||||||
"@#{user_2.username} @#{publicly_mentionable_group.name} ",
|
"@#{user_2.username} @#{publicly_mentionable_group.name} ",
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,6 +19,7 @@ module PageObjects
|
||||||
|
|
||||||
def visit_channel(channel, mobile: false)
|
def visit_channel(channel, mobile: false)
|
||||||
visit(channel.url + (mobile ? "?mobile_view=1" : ""))
|
visit(channel.url + (mobile ? "?mobile_view=1" : ""))
|
||||||
|
has_no_css?(".not-loaded-once")
|
||||||
has_no_css?(".chat-skeleton")
|
has_no_css?(".chat-skeleton")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.describe "Reply indicator", type: :system, js: true do
|
|
||||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
|
||||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
|
||||||
|
|
||||||
fab!(:channel_1) { Fabricate(:category_channel) }
|
|
||||||
fab!(:current_user) { Fabricate(:admin) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
chat_system_bootstrap
|
|
||||||
channel_1.add(current_user)
|
|
||||||
sign_in(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when clicking on a reply indicator of a loaded message" do
|
|
||||||
fab!(:replied_to_message) do
|
|
||||||
Fabricate(:chat_message, chat_channel: channel_1, created_at: 2.hours.ago)
|
|
||||||
end
|
|
||||||
fab!(:reply) do
|
|
||||||
Fabricate(
|
|
||||||
:chat_message,
|
|
||||||
chat_channel: channel_1,
|
|
||||||
in_reply_to: replied_to_message,
|
|
||||||
created_at: 1.minute.ago,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
10.times { Fabricate(:chat_message, chat_channel: channel_1, created_at: 1.hour.ago) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it "highlights the message without refreshing the pane" do
|
|
||||||
chat_page.visit_channel(channel_1)
|
|
||||||
|
|
||||||
find("[data-id='#{reply.id}'] .chat-reply").click
|
|
||||||
|
|
||||||
expect(page).to have_no_selector(".chat-skeleton")
|
|
||||||
expect(page).to have_selector("[data-id='#{replied_to_message.id}'].highlighted", wait: 0.1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,35 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.describe "Replying indicator", type: :system, js: true do
|
|
||||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
|
||||||
fab!(:current_user) { Fabricate(:user) }
|
|
||||||
fab!(:other_user) { Fabricate(:user) }
|
|
||||||
|
|
||||||
let(:chat) { PageObjects::Pages::Chat.new }
|
|
||||||
|
|
||||||
before do
|
|
||||||
channel_1.add(current_user)
|
|
||||||
channel_1.add(other_user)
|
|
||||||
chat_system_bootstrap
|
|
||||||
sign_in(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when on a channel" do
|
|
||||||
context "when another user is replying" do
|
|
||||||
it "shows the replying indicator" do
|
|
||||||
using_session(:user_1) do
|
|
||||||
sign_in(other_user)
|
|
||||||
chat.visit_channel(channel_1)
|
|
||||||
find(".chat-composer-input").fill_in(with: "hello there")
|
|
||||||
end
|
|
||||||
|
|
||||||
chat.visit_channel(channel_1)
|
|
||||||
|
|
||||||
expect(page).to have_selector(
|
|
||||||
".chat-replying-indicator",
|
|
||||||
text: I18n.t("js.chat.replying_indicator.single_user", username: other_user.username),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -93,8 +93,6 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
||||||
it "quotes the message" do
|
it "quotes the message" do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
clip_text = copy_messages_to_clipboard(message_1)
|
clip_text = copy_messages_to_clipboard(message_1)
|
||||||
topic_page.visit_topic_and_open_composer(post_1.topic)
|
topic_page.visit_topic_and_open_composer(post_1.topic)
|
||||||
topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
|
topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
|
||||||
|
@ -117,8 +115,6 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
||||||
it "quotes the messages" do
|
it "quotes the messages" do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
clip_text = copy_messages_to_clipboard([message_1, message_2])
|
clip_text = copy_messages_to_clipboard([message_1, message_2])
|
||||||
topic_page.visit_topic_and_open_composer(post_1.topic)
|
topic_page.visit_topic_and_open_composer(post_1.topic)
|
||||||
topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
|
topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
|
||||||
|
@ -149,8 +145,6 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
||||||
it "works" do
|
it "works" do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
clip_text = copy_messages_to_clipboard(message_1)
|
clip_text = copy_messages_to_clipboard(message_1)
|
||||||
topic_page.visit_topic_and_open_composer(post_1.topic)
|
topic_page.visit_topic_and_open_composer(post_1.topic)
|
||||||
topic_page.fill_in_composer(clip_text)
|
topic_page.fill_in_composer(clip_text)
|
||||||
|
@ -167,8 +161,6 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
||||||
it "quotes the message" do
|
it "quotes the message" do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
clip_text = copy_messages_to_clipboard(message_1)
|
clip_text = copy_messages_to_clipboard(message_1)
|
||||||
click_selection_button("cancel")
|
click_selection_button("cancel")
|
||||||
chat_channel_page.send_message(clip_text)
|
chat_channel_page.send_message(clip_text)
|
||||||
|
@ -191,8 +183,6 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
||||||
it "opens the topic composer with correct state" do
|
it "opens the topic composer with correct state" do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
select_message_desktop(message_1)
|
select_message_desktop(message_1)
|
||||||
click_selection_button("quote")
|
click_selection_button("quote")
|
||||||
|
|
||||||
|
@ -219,8 +209,6 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
||||||
mobile: true do
|
mobile: true do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
expect(chat_channel_page).to have_no_loading_skeleton
|
|
||||||
|
|
||||||
chat_channel_page.click_message_action_mobile(message_1, "selectMessage")
|
chat_channel_page.click_message_action_mobile(message_1, "selectMessage")
|
||||||
click_selection_button("quote")
|
click_selection_button("quote")
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
|
||||||
onHoverMessage: () => {},
|
onHoverMessage: () => {},
|
||||||
didShowMessage: () => {},
|
didShowMessage: () => {},
|
||||||
didHideMessage: () => {},
|
didHideMessage: () => {},
|
||||||
|
forceRendering: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +76,8 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
|
||||||
@onHoverMessage={{this.onHoverMessage}}
|
@onHoverMessage={{this.onHoverMessage}}
|
||||||
@didShowMessage={{this.didShowMessage}}
|
@didShowMessage={{this.didShowMessage}}
|
||||||
@didHideMessage={{this.didHideMessage}}
|
@didHideMessage={{this.didHideMessage}}
|
||||||
|
@didHideMessage={{this.didHideMessage}}
|
||||||
|
@forceRendering={{this.forceRendering}}
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue