mirror of
https://github.com/discourse/discourse.git
synced 2025-04-01 04:25:56 +08:00
UX: Merge the simplified topic map (#27964)
Replaces the existing topic map with the experimental-topic-map made by @awesomerobot. --------- Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
This commit is contained in:
parent
6039b513fe
commit
a027ec4663
app/assets
javascripts/discourse
app
tests
acceptance
integration/components/widgets
stylesheets
config/locales
spec/system
@ -1,131 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import PrivateMessageMap from "discourse/components/topic-map/private-message-map";
|
||||
import TopicMapExpanded from "discourse/components/topic-map/topic-map-expanded";
|
||||
import TopicMapSummary from "discourse/components/topic-map/topic-map-summary";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
const MIN_POST_READ_TIME = 4;
|
||||
|
||||
export default class TopicMap extends Component {
|
||||
@service siteSettings;
|
||||
@tracked collapsed = !this.args.model.has_summary;
|
||||
|
||||
get userFilters() {
|
||||
return this.args.postStream.userFilters || [];
|
||||
}
|
||||
|
||||
@action
|
||||
toggleMap() {
|
||||
this.collapsed = !this.collapsed;
|
||||
}
|
||||
|
||||
get topRepliesSummaryInfo() {
|
||||
if (this.topRepliesSummaryEnabled) {
|
||||
return I18n.t("summary.enabled_description");
|
||||
}
|
||||
|
||||
const wordCount = this.args.model.word_count;
|
||||
if (wordCount && this.siteSettings.read_time_word_count > 0) {
|
||||
const readingTime = Math.ceil(
|
||||
Math.max(
|
||||
wordCount / this.siteSettings.read_time_word_count,
|
||||
(this.args.model.posts_count * MIN_POST_READ_TIME) / 60
|
||||
)
|
||||
);
|
||||
return I18n.messageFormat("summary.description_time_MF", {
|
||||
replyCount: this.args.model.replyCount,
|
||||
readingTime,
|
||||
});
|
||||
}
|
||||
return I18n.t("summary.description", {
|
||||
count: this.args.model.replyCount,
|
||||
});
|
||||
}
|
||||
|
||||
get topRepliesTitle() {
|
||||
if (this.topRepliesSummaryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return I18n.t("summary.short_title");
|
||||
}
|
||||
|
||||
get topRepliesLabel() {
|
||||
const label = this.topRepliesSummaryEnabled
|
||||
? "summary.disable"
|
||||
: "summary.enable";
|
||||
|
||||
return I18n.t(label);
|
||||
}
|
||||
|
||||
get topRepliesIcon() {
|
||||
if (this.topRepliesSummaryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return "layer-group";
|
||||
}
|
||||
|
||||
<template>
|
||||
<section class={{concatClass "map" (if this.collapsed "map-collapsed")}}>
|
||||
<TopicMapSummary
|
||||
@topic={{@model}}
|
||||
@topicDetails={{@topicDetails}}
|
||||
@toggleMap={{this.toggleMap}}
|
||||
@collapsed={{this.collapsed}}
|
||||
@userFilters={{this.userFilters}}
|
||||
/>
|
||||
</section>
|
||||
{{#unless this.collapsed}}
|
||||
<section
|
||||
class="topic-map-expanded"
|
||||
id="topic-map-expanded__aria-controls"
|
||||
>
|
||||
<TopicMapExpanded
|
||||
@topicDetails={{@topicDetails}}
|
||||
@userFilters={{this.userFilters}}
|
||||
/>
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
<section class="information toggle-summary">
|
||||
{{#if @model.has_summary}}
|
||||
<p>{{htmlSafe this.topRepliesSummaryInfo}}</p>
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="topic-map-expanded-after"
|
||||
@defaultGlimmer={{true}}
|
||||
@outletArgs={{hash topic=@model postStream=@postStream}}
|
||||
>
|
||||
{{#if @model.has_summary}}
|
||||
<DButton
|
||||
@action={{if @postStream.summary @cancelFilter @showTopReplies}}
|
||||
@translatedTitle={{this.topRepliesTitle}}
|
||||
@translatedLabel={{this.topRepliesLabel}}
|
||||
@icon={{this.topRepliesIcon}}
|
||||
class="top-replies"
|
||||
/>
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
||||
</section>
|
||||
|
||||
{{#if @showPMMap}}
|
||||
<section class="information private-message-map">
|
||||
<PrivateMessageMap
|
||||
@topicDetails={{@topicDetails}}
|
||||
@showInvite={{@showInvite}}
|
||||
@removeAllowedGroup={{@removeAllowedGroup}}
|
||||
@removeAllowedUser={{@removeAllowedUser}}
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { hash } from "@ember/helper";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import PrivateMessageMap from "discourse/components/topic-map/private-message-map";
|
||||
import TopicMapSummary from "discourse/components/topic-map/topic-map-summary";
|
||||
|
||||
const TopicMap = <template>
|
||||
{{#unless @model.postStream.loadingFilter}}
|
||||
<section class="topic-map__contents">
|
||||
<TopicMapSummary
|
||||
@topic={{@model}}
|
||||
@topicDetails={{@topicDetails}}
|
||||
@postStream={{@postStream}}
|
||||
/>
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
<section class="topic-map__additional-contents toggle-summary">
|
||||
<PluginOutlet
|
||||
@name="topic-map-expanded-after"
|
||||
@defaultGlimmer={{true}}
|
||||
@outletArgs={{hash topic=@model postStream=@postStream}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{{#if @showPMMap}}
|
||||
<section class="topic-map__private-message-map">
|
||||
<PrivateMessageMap
|
||||
@topicDetails={{@topicDetails}}
|
||||
@showInvite={{@showInvite}}
|
||||
@removeAllowedGroup={{@removeAllowedGroup}}
|
||||
@removeAllowedUser={{@removeAllowedUser}}
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default TopicMap;
|
@ -1,133 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import TopicParticipants from "discourse/components/topic-map/topic-participants";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import lt from "truth-helpers/helpers/lt";
|
||||
import not from "truth-helpers/helpers/not";
|
||||
|
||||
const TRUNCATED_LINKS_LIMIT = 5;
|
||||
|
||||
export default class TopicMapExpanded extends Component {
|
||||
@tracked allLinksShown = false;
|
||||
|
||||
get topicLinks() {
|
||||
return this.args.topicDetails.links;
|
||||
}
|
||||
|
||||
get participants() {
|
||||
return this.args.topicDetails.participants;
|
||||
}
|
||||
|
||||
@action
|
||||
showAllLinks() {
|
||||
this.allLinksShown = true;
|
||||
}
|
||||
|
||||
get linksToShow() {
|
||||
return this.allLinksShown
|
||||
? this.topicLinks
|
||||
: this.topicLinks.slice(0, TRUNCATED_LINKS_LIMIT);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.participants}}
|
||||
<section class="avatars">
|
||||
<TopicParticipants
|
||||
@title={{i18n "topic_map.participants_title"}}
|
||||
@userFilters={{@userFilters}}
|
||||
@participants={{this.participants}}
|
||||
/>
|
||||
</section>
|
||||
{{/if}}
|
||||
{{#if this.topicLinks}}
|
||||
<section class="links">
|
||||
<h3>{{i18n "topic_map.links_title"}}</h3>
|
||||
<table class="topic-links">
|
||||
<tbody>
|
||||
{{#each this.linksToShow as |link|}}
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class="badge badge-notification clicks"
|
||||
title={{i18n "topic_map.clicks" count=link.clicks}}
|
||||
>
|
||||
{{link.clicks}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<TopicMapLink
|
||||
@attachment={{link.attachment}}
|
||||
@title={{link.title}}
|
||||
@rootDomain={{link.root_domain}}
|
||||
@url={{link.url}}
|
||||
@userId={{link.user_id}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{#if
|
||||
(and
|
||||
(not this.allLinksShown)
|
||||
(lt TRUNCATED_LINKS_LIMIT this.topicLinks.length)
|
||||
)
|
||||
}}
|
||||
<div class="link-summary">
|
||||
<span>
|
||||
<DButton
|
||||
@action={{this.showAllLinks}}
|
||||
@title="topic_map.links_shown"
|
||||
@icon="chevron-down"
|
||||
class="btn-flat"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
class TopicMapLink extends Component {
|
||||
get linkClasses() {
|
||||
return this.args.attachment
|
||||
? "topic-link track-link attachment"
|
||||
: "topic-link track-link";
|
||||
}
|
||||
|
||||
get truncatedContent() {
|
||||
const truncateLength = 85;
|
||||
const content = this.args.title || this.args.url;
|
||||
return content.length > truncateLength
|
||||
? `${content.slice(0, truncateLength).trim()}...`
|
||||
: content;
|
||||
}
|
||||
|
||||
<template>
|
||||
<a
|
||||
class={{this.linkClasses}}
|
||||
href={{@url}}
|
||||
title={{@url}}
|
||||
data-user-id={{@userId}}
|
||||
data-ignore-post-id="true"
|
||||
target="_blank"
|
||||
rel="nofollow ugc noopener noreferrer"
|
||||
>
|
||||
{{#if @title}}
|
||||
{{replaceEmoji this.truncatedContent}}
|
||||
{{else}}
|
||||
{{this.truncatedContent}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{#if (and @title @rootDomain)}}
|
||||
<span class="domain">
|
||||
{{@rootDomain}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import Component from "@glimmer/component";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
|
||||
const TRUNCATE_LENGTH_LIMIT = 85;
|
||||
|
||||
export default class TopicMapLink extends Component {
|
||||
get linkClasses() {
|
||||
return this.args.attachment
|
||||
? "topic-link track-link attachment"
|
||||
: "topic-link track-link";
|
||||
}
|
||||
|
||||
get truncatedContent() {
|
||||
const content = this.args.title || this.args.url;
|
||||
return content.length > TRUNCATE_LENGTH_LIMIT
|
||||
? `${content.slice(0, TRUNCATE_LENGTH_LIMIT).trim()}...`
|
||||
: content;
|
||||
}
|
||||
|
||||
<template>
|
||||
<a
|
||||
class={{this.linkClasses}}
|
||||
href={{@url}}
|
||||
title={{@url}}
|
||||
data-user-id={{@userId}}
|
||||
data-ignore-post-id="true"
|
||||
target="_blank"
|
||||
rel="nofollow ugc noopener noreferrer"
|
||||
>
|
||||
{{#if @title}}
|
||||
{{replaceEmoji this.truncatedContent}}
|
||||
{{else}}
|
||||
{{this.truncatedContent}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{#if (and @title @rootDomain)}}
|
||||
<span class="domain">
|
||||
{{@rootDomain}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -1,162 +1,440 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { gt } from "truth-helpers";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import RelativeDate from "discourse/components/relative-date";
|
||||
import TopicMapLink from "discourse/components/topic-map/topic-map-link";
|
||||
import TopicParticipants from "discourse/components/topic-map/topic-participants";
|
||||
import TopicViews from "discourse/components/topic-map/topic-views";
|
||||
import TopicViewsChart from "discourse/components/topic-map/topic-views-chart";
|
||||
import avatar from "discourse/helpers/bound-avatar-template";
|
||||
import number from "discourse/helpers/number";
|
||||
import slice from "discourse/helpers/slice";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { avatarImg } from "discourse-common/lib/avatar-utils";
|
||||
import I18n from "discourse-i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
const TRUNCATED_LINKS_LIMIT = 5;
|
||||
const MIN_POST_READ_TIME_MINUTES = 4;
|
||||
const MIN_READ_TIME_MINUTES = 3;
|
||||
const MIN_LIKES_COUNT = 5;
|
||||
const MIN_PARTICIPANTS_COUNT = 5;
|
||||
const MIN_USERS_COUNT_FOR_AVATARS = 2;
|
||||
|
||||
export const MIN_POSTS_COUNT = 10;
|
||||
|
||||
export default class TopicMapSummary extends Component {
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service mapCache;
|
||||
@service dialog;
|
||||
|
||||
@tracked allLinksShown = false;
|
||||
@tracked top3LikedPosts = [];
|
||||
@tracked views = [];
|
||||
@tracked loading = true;
|
||||
|
||||
get shouldShowParticipants() {
|
||||
return (
|
||||
this.args.topic.posts_count >= MIN_POSTS_COUNT &&
|
||||
this.args.topicDetails.participants?.length >=
|
||||
MIN_USERS_COUNT_FOR_AVATARS &&
|
||||
!this.site.mobileView
|
||||
);
|
||||
}
|
||||
|
||||
get first5Participants() {
|
||||
return this.args.topicDetails.participants.slice(0, MIN_PARTICIPANTS_COUNT);
|
||||
}
|
||||
|
||||
get readTimeMinutes() {
|
||||
const calculatedTime = Math.ceil(
|
||||
Math.max(
|
||||
this.args.topic.word_count / this.siteSettings.read_time_word_count,
|
||||
(this.args.topic.posts_count * MIN_POST_READ_TIME_MINUTES) / 60
|
||||
)
|
||||
);
|
||||
|
||||
return calculatedTime > MIN_READ_TIME_MINUTES ? calculatedTime : null;
|
||||
}
|
||||
|
||||
get topRepliesSummaryEnabled() {
|
||||
return this.args.postStream.summary;
|
||||
}
|
||||
|
||||
get topRepliesTitle() {
|
||||
if (this.topRepliesSummaryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return I18n.t("summary.short_title");
|
||||
}
|
||||
|
||||
get topRepliesIcon() {
|
||||
return this.topRepliesSummaryEnabled ? "arrows-alt-v" : "layer-group";
|
||||
}
|
||||
|
||||
get topRepliesLabel() {
|
||||
return this.topRepliesSummaryEnabled
|
||||
? I18n.t("summary.show_all_label")
|
||||
: I18n.t("summary.short_label");
|
||||
}
|
||||
|
||||
get loneStat() {
|
||||
if (this.args.topic.has_summary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
[this.hasViews, this.hasLikes, this.hasUsers, this.hasLinks].filter(
|
||||
Boolean
|
||||
).length === 1
|
||||
);
|
||||
}
|
||||
|
||||
get shouldShowViewsChart() {
|
||||
return this.views.stats.length > 2;
|
||||
}
|
||||
|
||||
get linksCount() {
|
||||
return this.args.topicDetails.links?.length ?? 0;
|
||||
}
|
||||
|
||||
get createdByUsername() {
|
||||
return this.args.topicDetails.created_by?.username;
|
||||
get topicLinks() {
|
||||
return this.args.topicDetails.links;
|
||||
}
|
||||
|
||||
get lastPosterUsername() {
|
||||
return this.args.topicDetails.last_poster?.username;
|
||||
get linksToShow() {
|
||||
return this.allLinksShown
|
||||
? this.topicLinks
|
||||
: this.topicLinks?.slice(0, TRUNCATED_LINKS_LIMIT);
|
||||
}
|
||||
|
||||
get toggleMapButton() {
|
||||
return {
|
||||
title: this.args.collapsed
|
||||
? "topic.expand_details"
|
||||
: "topic.collapse_details",
|
||||
icon: this.args.collapsed ? "chevron-down" : "chevron-up",
|
||||
ariaExpanded: this.args.collapsed ? "false" : "true",
|
||||
ariaControls: "topic-map-expanded__aria-controls",
|
||||
action: this.args.toggleMap,
|
||||
};
|
||||
get hasMoreLinks() {
|
||||
return !this.allLinksShown && this.linksCount > TRUNCATED_LINKS_LIMIT;
|
||||
}
|
||||
|
||||
get shouldShowParticipants() {
|
||||
get hasViews() {
|
||||
return this.args.topic.views > 1;
|
||||
}
|
||||
|
||||
get hasLikes() {
|
||||
return (
|
||||
this.args.collapsed &&
|
||||
this.args.topic.posts_count > 2 &&
|
||||
this.args.topicDetails.participants &&
|
||||
this.args.topicDetails.participants.length > 0
|
||||
this.args.topic.like_count > MIN_LIKES_COUNT &&
|
||||
this.args.topic.posts_count > MIN_POSTS_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
get createdByAvatar() {
|
||||
return htmlSafe(
|
||||
avatarImg({
|
||||
avatarTemplate: this.args.topicDetails.created_by?.avatar_template,
|
||||
size: "tiny",
|
||||
title:
|
||||
this.args.topicDetails.created_by?.name ||
|
||||
this.args.topicDetails.created_by?.username,
|
||||
})
|
||||
);
|
||||
get hasUsers() {
|
||||
return this.args.topic.participant_count > MIN_PARTICIPANTS_COUNT;
|
||||
}
|
||||
|
||||
get lastPostAvatar() {
|
||||
return htmlSafe(
|
||||
avatarImg({
|
||||
avatarTemplate: this.args.topicDetails.last_poster?.avatar_template,
|
||||
size: "tiny",
|
||||
title:
|
||||
this.args.topicDetails.last_poster?.name ||
|
||||
this.args.topicDetails.last_poster?.username,
|
||||
get hasLinks() {
|
||||
return this.linksCount > 0;
|
||||
}
|
||||
|
||||
@action
|
||||
showAllLinks() {
|
||||
this.allLinksShown = true;
|
||||
}
|
||||
|
||||
@action
|
||||
showTopReplies() {
|
||||
this.args.postStream.showTopReplies();
|
||||
}
|
||||
|
||||
@action
|
||||
cancelFilter() {
|
||||
this.args.postStream.cancelFilter();
|
||||
this.args.postStream.refresh();
|
||||
}
|
||||
|
||||
@action
|
||||
postUrl(post) {
|
||||
return this.args.topic.urlForPostNumber(post.post_number);
|
||||
}
|
||||
|
||||
@action
|
||||
fetchMostLiked() {
|
||||
const cacheKey = `top3LikedPosts_${this.args.topic.id}`;
|
||||
const cachedData = this.mapCache.get(cacheKey);
|
||||
this.loading = true;
|
||||
|
||||
if (cachedData) {
|
||||
this.top3LikedPosts = cachedData;
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = `/search.json?q=" " topic%3A${this.args.topic.id} order%3Alikes`;
|
||||
|
||||
ajax(filter)
|
||||
.then((data) => {
|
||||
const top3LikedPosts = data.posts
|
||||
.filter((post) => post.post_number > 1 && post.like_count > 0)
|
||||
.sort((a, b) => b.like_count - a.like_count)
|
||||
.slice(0, 3);
|
||||
|
||||
this.mapCache.set(cacheKey, top3LikedPosts);
|
||||
this.top3LikedPosts = top3LikedPosts;
|
||||
})
|
||||
);
|
||||
.catch((error) => {
|
||||
this.dialog.alert(
|
||||
I18n.t("generic_error_with_reason", {
|
||||
error: `http: ${error.status} - ${error.body}`,
|
||||
})
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
fetchViews() {
|
||||
const cacheKey = `topicViews_${this.args.topic.id}`;
|
||||
const cachedData = this.mapCache.get(cacheKey);
|
||||
this.loading = true;
|
||||
|
||||
if (cachedData) {
|
||||
this.views = cachedData;
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ajax(`/t/${this.args.topic.id}/view-stats.json`)
|
||||
.then((data) => {
|
||||
if (data.stats.length) {
|
||||
this.views = data;
|
||||
} else {
|
||||
data.stats.push({
|
||||
viewed_at: new Date().toISOString().split("T")[0],
|
||||
views: this.args.topic.views,
|
||||
});
|
||||
this.views = data;
|
||||
}
|
||||
this.mapCache.set(cacheKey, data);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.dialog.alert(
|
||||
I18n.t("generic_error_with_reason", {
|
||||
error: `http: ${error.status} - ${error.body}`,
|
||||
})
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<nav class="buttons">
|
||||
<DButton
|
||||
@icon={{this.toggleMapButton.icon}}
|
||||
@title={{this.toggleMapButton.title}}
|
||||
@ariaExpanded={{this.toggleMapButton.ariaExpanded}}
|
||||
@ariaControls={{this.toggleMapButton.ariaControls}}
|
||||
@action={{this.toggleMapButton.action}}
|
||||
class="btn"
|
||||
/>
|
||||
</nav>
|
||||
<ul>
|
||||
<li class="created-at">
|
||||
<h4 role="presentation">{{i18n "created_lowercase"}}</h4>
|
||||
<div class="topic-map-post created-at">
|
||||
<a
|
||||
class="trigger-user-card"
|
||||
data-user-card={{this.createdByUsername}}
|
||||
title={{this.createdByUsername}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{this.createdByAvatar}}
|
||||
<RelativeDate @date={{@topic.created_at}} />
|
||||
</div>
|
||||
</li>
|
||||
<li class="last-reply">
|
||||
<a href={{@topic.lastPostUrl}}>
|
||||
<h4 role="presentation">{{i18n "last_reply_lowercase"}}</h4>
|
||||
<div class="topic-map-post last-reply">
|
||||
<a
|
||||
class="trigger-user-card"
|
||||
data-user-card={{this.lastPosterUsername}}
|
||||
title={{this.lastPosterUsername}}
|
||||
aria-hidden="true"
|
||||
<div class="topic-map__stats {{if this.loneStat '--single-stat'}}">
|
||||
<DMenu
|
||||
@arrow={{true}}
|
||||
@identifier="topic-map__views"
|
||||
@interactive={{true}}
|
||||
@triggers="click"
|
||||
@modalForMobile={{true}}
|
||||
@placement="right"
|
||||
@groupIdentifier="topic-map"
|
||||
@inline={{true}}
|
||||
@onShow={{this.fetchViews}}
|
||||
>
|
||||
<:trigger>
|
||||
{{number @topic.views noTitle="true"}}
|
||||
<span class="topic-map__stat-label">
|
||||
{{i18n "views_lowercase" count=@topic.views}}
|
||||
</span>
|
||||
</:trigger>
|
||||
<:content>
|
||||
<h3>{{i18n "topic_map.menu_titles.views"}}</h3>
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
{{#if this.shouldShowViewsChart}}
|
||||
<TopicViewsChart
|
||||
@views={{this.views}}
|
||||
@created={{@topic.created_at}}
|
||||
/>
|
||||
{{else}}
|
||||
<TopicViews @views={{this.views}} />
|
||||
{{/if}}
|
||||
</ConditionalLoadingSpinner>
|
||||
</:content>
|
||||
</DMenu>
|
||||
|
||||
{{#if this.hasLikes}}
|
||||
<DMenu
|
||||
@arrow={{true}}
|
||||
@identifier="topic-map__likes"
|
||||
@interactive={{true}}
|
||||
@triggers="click"
|
||||
@modalForMobile={{true}}
|
||||
@placement="right"
|
||||
@groupIdentifier="topic-map"
|
||||
@inline={{true}}
|
||||
>
|
||||
<:trigger>
|
||||
{{number @topic.like_count noTitle="true"}}
|
||||
<span class="topic-map__stat-label">
|
||||
{{i18n "likes_lowercase" count=@topic.like_count}}
|
||||
</span>
|
||||
</:trigger>
|
||||
<:content>
|
||||
<h3 {{didInsert this.fetchMostLiked}}>{{i18n
|
||||
"topic_map.menu_titles.replies"
|
||||
}}</h3>
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
<ul>
|
||||
{{#each this.top3LikedPosts as |post|}}
|
||||
<li>
|
||||
<a href={{this.postUrl post}}>
|
||||
<span class="like-section__user">
|
||||
{{avatar
|
||||
post.avatar_template
|
||||
"tiny"
|
||||
(hash title=post.username)
|
||||
}}
|
||||
{{post.username}}
|
||||
</span>
|
||||
<span class="like-section__likes">
|
||||
{{post.like_count}}
|
||||
{{dIcon "heart"}}</span>
|
||||
<p>
|
||||
{{htmlSafe (emojiUnescape post.blurb)}}
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</ConditionalLoadingSpinner>
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.linksCount}}
|
||||
<DMenu
|
||||
@arrow={{true}}
|
||||
@identifier="topic-map__links"
|
||||
@interactive={{true}}
|
||||
@triggers="click"
|
||||
@modalForMobile={{true}}
|
||||
@groupIdentifier="topic-map"
|
||||
@placement="right"
|
||||
@inline={{true}}
|
||||
>
|
||||
<:trigger>
|
||||
{{number this.linksCount noTitle="true"}}
|
||||
<span class="topic-map__stat-label">
|
||||
{{i18n "links_lowercase" count=this.linksCount}}
|
||||
</span>
|
||||
</:trigger>
|
||||
<:content>
|
||||
<h3>{{i18n "topic_map.links_title"}}</h3>
|
||||
<table class="topic-links">
|
||||
<tbody>
|
||||
{{#each this.linksToShow as |link|}}
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class="badge badge-notification clicks"
|
||||
title={{i18n "topic_map.clicks" count=link.clicks}}
|
||||
>
|
||||
{{link.clicks}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<TopicMapLink
|
||||
@attachment={{link.attachment}}
|
||||
@title={{link.title}}
|
||||
@rootDomain={{link.root_domain}}
|
||||
@url={{link.url}}
|
||||
@userId={{link.user_id}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{#if this.hasMoreLinks}}
|
||||
<div class="link-summary">
|
||||
<span>
|
||||
<DButton
|
||||
@action={{this.showAllLinks}}
|
||||
@title="topic_map.links_shown"
|
||||
@icon="chevron-down"
|
||||
class="btn-flat"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.hasUsers}}
|
||||
<DMenu
|
||||
@arrow={{true}}
|
||||
@identifier="topic-map__users"
|
||||
@interactive={{true}}
|
||||
@triggers="click"
|
||||
@placement="right"
|
||||
@modalForMobile={{true}}
|
||||
@groupIdentifier="topic-map"
|
||||
@inline={{true}}
|
||||
>
|
||||
<:trigger>
|
||||
{{number @topic.participant_count noTitle="true"}}
|
||||
<span class="topic-map__stat-label">
|
||||
{{i18n "users_lowercase" count=@topic.participant_count}}
|
||||
</span>
|
||||
</:trigger>
|
||||
<:content>
|
||||
<TopicParticipants
|
||||
@title={{i18n "topic_map.participants_title"}}
|
||||
@userFilters={{@userFilters}}
|
||||
@participants={{@topicDetails.participants}}
|
||||
/>
|
||||
{{this.lastPostAvatar}}
|
||||
<RelativeDate @date={{@topic.last_posted_at}} />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="replies">
|
||||
{{number @topic.replyCount noTitle="true"}}
|
||||
<h4 role="presentation">{{i18n
|
||||
"replies_lowercase"
|
||||
count=@topic.replyCount
|
||||
}}</h4>
|
||||
</li>
|
||||
<li class="secondary views">
|
||||
{{number @topic.views noTitle="true" class=@topic.viewsHeat}}
|
||||
<h4 role="presentation">{{i18n
|
||||
"views_lowercase"
|
||||
count=@topic.views
|
||||
}}</h4>
|
||||
</li>
|
||||
{{#if (gt @topic.participant_count 0)}}
|
||||
<li class="secondary users">
|
||||
{{number @topic.participant_count noTitle="true"}}
|
||||
<h4 role="presentation">{{i18n
|
||||
"users_lowercase"
|
||||
count=@topic.participant_count
|
||||
}}</h4>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (gt @topic.like_count 0)}}
|
||||
<li class="secondary likes">
|
||||
{{number @topic.like_count noTitle="true"}}
|
||||
<h4 role="presentation">{{i18n
|
||||
"likes_lowercase"
|
||||
count=@topic.like_count
|
||||
}}</h4>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (gt this.linksCount 0)}}
|
||||
<li class="secondary links">
|
||||
{{number this.linksCount noTitle="true"}}
|
||||
<h4 role="presentation">{{i18n
|
||||
"links_lowercase"
|
||||
count=this.linksCount
|
||||
}}</h4>
|
||||
</li>
|
||||
</:content>
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.shouldShowParticipants}}
|
||||
<li class="avatars">
|
||||
<TopicParticipants
|
||||
@participants={{slice 0 3 @topicDetails.participants}}
|
||||
@userFilters={{@userFilters}}
|
||||
/>
|
||||
</li>
|
||||
<TopicParticipants
|
||||
@participants={{this.first5Participants}}
|
||||
@userFilters={{@userFilters}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<div class="topic-map__buttons">
|
||||
{{#if this.readTimeMinutes}}
|
||||
<div class="estimated-read-time">
|
||||
<span> {{i18n "topic_map.read"}} </span>
|
||||
<span>
|
||||
{{this.readTimeMinutes}}
|
||||
{{i18n "topic_map.minutes"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="summarization-buttons">
|
||||
{{#if @topic.has_summary}}
|
||||
<DButton
|
||||
@action={{if
|
||||
@postStream.summary
|
||||
this.cancelFilter
|
||||
this.showTopReplies
|
||||
}}
|
||||
@translatedTitle={{this.topRepliesTitle}}
|
||||
@translatedLabel={{this.topRepliesLabel}}
|
||||
@icon={{this.topRepliesIcon}}
|
||||
class="top-replies"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ export default class TopicParticipants extends Component {
|
||||
{{#if @title}}
|
||||
<h3>{{@title}}</h3>
|
||||
{{/if}}
|
||||
{{#each @participants as |participant|}}
|
||||
<TopicParticipant
|
||||
@participant={{participant}}
|
||||
@toggledUsers={{this.toggledUsers}}
|
||||
/>
|
||||
{{/each}}
|
||||
<div class="topic-map__users-list {{unless @title '--users-summary'}}">
|
||||
{{#each @participants as |participant|}}
|
||||
<TopicParticipant
|
||||
@participant={{participant}}
|
||||
@toggledUsers={{this.toggledUsers}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
@ -0,0 +1,227 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
const oneDay = 86400000; // day in milliseconds
|
||||
|
||||
const now = new Date();
|
||||
const startOfDay = Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate()
|
||||
);
|
||||
|
||||
function fillMissingDates(data) {
|
||||
const filledData = [];
|
||||
let currentDate = data[0].x;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
while (currentDate < data[i].x) {
|
||||
filledData.push({ x: currentDate, y: 0 });
|
||||
currentDate += oneDay;
|
||||
}
|
||||
filledData.push(data[i]);
|
||||
currentDate = data[i].x + oneDay;
|
||||
}
|
||||
|
||||
return filledData;
|
||||
}
|
||||
|
||||
function weightedMovingAverage(data, period = 3) {
|
||||
const weights = Array.from({ length: period }, (_, i) => i + 1);
|
||||
const weightSum = weights.reduce((a, b) => a + b, 0);
|
||||
let result = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (i < period - 1) {
|
||||
result.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
let weightedSum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
weightedSum += data[i - j].y * weights[j];
|
||||
}
|
||||
|
||||
result.push(Math.round(weightedSum / weightSum));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function predictTodaysViews(data) {
|
||||
const movingAvg = weightedMovingAverage(data);
|
||||
const lastMovingAvg = movingAvg[movingAvg.length - 1];
|
||||
const currentViews = data[data.length - 1].y;
|
||||
const currentTimeUTC = Date.now() + now.getTimezoneOffset() * 60 * 1000;
|
||||
const elapsedTime = (currentTimeUTC - startOfDay) / oneDay; // amount of day passed
|
||||
let adjustedPrediction = lastMovingAvg;
|
||||
|
||||
if (currentViews >= lastMovingAvg) {
|
||||
// If higher than the average prediction, extrapolate
|
||||
adjustedPrediction =
|
||||
currentViews + (currentViews - lastMovingAvg) * (1 - elapsedTime);
|
||||
} else {
|
||||
// If views are lower than the average, adjust towards average
|
||||
adjustedPrediction = currentViews + lastMovingAvg * (1 - elapsedTime);
|
||||
}
|
||||
return Math.round(Math.max(adjustedPrediction, currentViews)); // never lower than actual data
|
||||
}
|
||||
|
||||
export default class TopicViewsChart extends Component {
|
||||
chart = null;
|
||||
noData = false;
|
||||
|
||||
@action
|
||||
async renderChart(element) {
|
||||
await loadScript("/javascripts/Chart.min.js");
|
||||
|
||||
if (!this.args.views?.stats || this.args.views?.stats?.length === 0) {
|
||||
this.noData = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let data = this.args.views.stats.map((item) => ({
|
||||
x: new Date(`${item.viewed_at}T00:00:00Z`).getTime(), // Use UTC time
|
||||
y: item.views,
|
||||
}));
|
||||
|
||||
data = fillMissingDates(data);
|
||||
|
||||
const lastDay = data[data.length - 1];
|
||||
|
||||
const predictedViews = predictTodaysViews(data);
|
||||
const predictedDataPoint = {
|
||||
x: lastDay.x,
|
||||
y: predictedViews,
|
||||
};
|
||||
|
||||
// remove current day's actual point, we'll replace with prediction
|
||||
data = data.slice(0, data.length - 1);
|
||||
// Add predicted data point
|
||||
data.push(predictedDataPoint);
|
||||
|
||||
const context = element.getContext("2d");
|
||||
|
||||
const xMin = data[0].x;
|
||||
const xMax = lastDay.x;
|
||||
|
||||
const topicMapElement = document.querySelector(".topic-map");
|
||||
|
||||
// grab colors from CSS
|
||||
const lineColor =
|
||||
getComputedStyle(topicMapElement).getPropertyValue("--chart-line-color");
|
||||
const pointColor = getComputedStyle(topicMapElement).getPropertyValue(
|
||||
"--chart-point-color"
|
||||
);
|
||||
const predictionColor = getComputedStyle(topicMapElement).getPropertyValue(
|
||||
"--chart-prediction-color"
|
||||
);
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
|
||||
this.chart = new window.Chart(context, {
|
||||
type: "line",
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Views",
|
||||
data: data.slice(0, -1),
|
||||
showLine: true,
|
||||
borderColor: pointColor,
|
||||
backgroundColor: lineColor,
|
||||
pointBackgroundColor: pointColor,
|
||||
},
|
||||
{
|
||||
label: "Predicted Views",
|
||||
data: [data[data.length - 2], data[data.length - 1]],
|
||||
showLine: true,
|
||||
borderDash: [5, 5],
|
||||
borderColor: predictionColor,
|
||||
backgroundColor: predictionColor,
|
||||
pointBackgroundColor: predictionColor,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: "linear",
|
||||
position: "bottom",
|
||||
min: xMin,
|
||||
max: xMax,
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
stepSize: oneDay,
|
||||
maxTicksLimit: 15,
|
||||
callback: function (value) {
|
||||
const date = new Date(value + oneDay);
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return value;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: function (tooltipItem) {
|
||||
let date = new Date(tooltipItem[0]?.parsed?.x + oneDay);
|
||||
if (tooltipItem.length === 0) {
|
||||
const today = new Date();
|
||||
date = today.getUTCDate();
|
||||
}
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
},
|
||||
label: function (tooltipItem) {
|
||||
const label =
|
||||
tooltipItem?.parsed?.x === startOfDay
|
||||
? I18n.t("topic_map.predicted_views")
|
||||
: I18n.t("topic_map.views");
|
||||
|
||||
return `${label}: ${tooltipItem?.parsed?.y}`;
|
||||
},
|
||||
},
|
||||
filter: function (tooltipItem) {
|
||||
return !(
|
||||
tooltipItem?.parsed?.x === startOfDay - oneDay &&
|
||||
tooltipItem?.datasetIndex === 1
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.noData}}
|
||||
{{i18n "topic_map.chart_error"}}
|
||||
{{else}}
|
||||
<canvas {{didInsert this.renderChart}}></canvas>
|
||||
<div class="view-explainer">{{i18n "topic_map.view_explainer"}}</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
export default class TopicViews extends Component {
|
||||
adjustAggregatedData(stats) {
|
||||
const adjustedStats = [];
|
||||
|
||||
stats.forEach((stat) => {
|
||||
const localDate = new Date(`${stat.viewed_at}T00:00:00Z`);
|
||||
const localDateStr = localDate.toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const existingStat = adjustedStats.find(
|
||||
(s) => s.dateStr === localDateStr
|
||||
);
|
||||
|
||||
if (existingStat) {
|
||||
existingStat.views += stat.views;
|
||||
} else {
|
||||
adjustedStats.push({
|
||||
dateStr: localDateStr,
|
||||
views: stat.views,
|
||||
localDate,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return adjustedStats.map((stat) => ({
|
||||
viewed_at: stat.localDate.toISOString().split("T")[0],
|
||||
views: stat.views,
|
||||
}));
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
get updatedStats() {
|
||||
const adjustedStats = this.adjustAggregatedData(this.args.views.stats);
|
||||
|
||||
let stats = adjustedStats.map((stat) => {
|
||||
const statDate = new Date(`${stat.viewed_at}T00:00:00`).getTime();
|
||||
const localStatDate = new Date(statDate);
|
||||
|
||||
return {
|
||||
...stat,
|
||||
statDate: localStatDate,
|
||||
label: this.formatDate(localStatDate),
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="topic-views__wrapper">
|
||||
{{#each this.updatedStats as |stat|}}
|
||||
<div class="topic-views">
|
||||
<div class="topic-views__count">
|
||||
{{stat.views}}
|
||||
</div>
|
||||
<div class="topic-views__date">
|
||||
{{stat.label}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -14,6 +14,7 @@ import ChangePostNoticeModal from "discourse/components/modal/change-post-notice
|
||||
import ConvertToPublicTopicModal from "discourse/components/modal/convert-to-public-topic";
|
||||
import DeleteTopicConfirmModal from "discourse/components/modal/delete-topic-confirm";
|
||||
import JumpToPost from "discourse/components/modal/jump-to-post";
|
||||
import { MIN_POSTS_COUNT } from "discourse/components/topic-map/topic-map-summary";
|
||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
@ -239,6 +240,11 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
return Category.findById(categoryId)?.minimumRequiredTags || 0;
|
||||
},
|
||||
|
||||
@discourseComputed("model.posts_count")
|
||||
showBottomTopicMap(postsCount) {
|
||||
return postsCount > MIN_POSTS_COUNT;
|
||||
},
|
||||
|
||||
_removeDeleteOnOwnerReplyBookmarks() {
|
||||
// the user has already navigated away from the topic. the PostCreator
|
||||
// in rails already handles deleting the bookmarks that need to be
|
||||
|
35
app/assets/javascripts/discourse/app/services/map-cache.js
Normal file
35
app/assets/javascripts/discourse/app/services/map-cache.js
Normal file
@ -0,0 +1,35 @@
|
||||
import Service from "@ember/service";
|
||||
|
||||
export default class MapCache extends Service {
|
||||
cache = {};
|
||||
|
||||
get(key) {
|
||||
const cachedItem = this.cache[key];
|
||||
if (!cachedItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { value, timestamp, ttl } = cachedItem;
|
||||
const now = Date.now();
|
||||
|
||||
if (now - timestamp > ttl) {
|
||||
this.clear(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key, value, ttl = 120000) {
|
||||
// expires after 2 min
|
||||
this.cache[key] = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
ttl,
|
||||
};
|
||||
}
|
||||
|
||||
clear(key) {
|
||||
delete this.cache[key];
|
||||
}
|
||||
}
|
@ -211,6 +211,20 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.showBottomTopicMap}}
|
||||
<div class="topic-map --bottom">
|
||||
<TopicMap
|
||||
@model={{this.model}}
|
||||
@topicDetails={{this.model.details}}
|
||||
@postStream={{this.model.postStream}}
|
||||
@showPMMap={{eq this.model.archetype "private_message"}}
|
||||
@showInvite={{route-action "showInvite"}}
|
||||
@removeAllowedGroup={{action "removeAllowedGroup"}}
|
||||
@removeAllowedUser={{action "removeAllowedUser"}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet @name="above-timeline" @connectorTagName="div" />
|
||||
|
||||
<TopicNavigation
|
||||
|
@ -743,43 +743,9 @@ createWidget("post-body", {
|
||||
result.push(postContents);
|
||||
result.push(this.attach("actions-summary", attrs));
|
||||
result.push(this.attach("post-links", attrs));
|
||||
if (attrs.showTopicMap) {
|
||||
result.push(this.buildTopicMap(attrs));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
buildTopicMap(attrs) {
|
||||
return new RenderGlimmer(
|
||||
this,
|
||||
"div.topic-map",
|
||||
hbs`<TopicMap
|
||||
@model={{@data.model}}
|
||||
@topicDetails={{@data.topicDetails}}
|
||||
@postStream={{@data.postStream}}
|
||||
@showPMMap={{@data.showPMMap}}
|
||||
@cancelFilter={{@data.cancelFilter}}
|
||||
@showTopReplies={{@data.showTopReplies}}
|
||||
@showInvite={{@data.showInvite}}
|
||||
@removeAllowedGroup={{@data.removeAllowedGroup}}
|
||||
@removeAllowedUser={{@data.removeAllowedUser}}
|
||||
/>`,
|
||||
{
|
||||
model: attrs.topic,
|
||||
topicDetails: attrs.topic.get("details"),
|
||||
postStream: attrs.topic.postStream,
|
||||
showPMMap: attrs.showPMMap,
|
||||
cancelFilter: () => this.sendWidgetAction("cancelFilter"),
|
||||
showTopReplies: () => this.sendWidgetAction("showTopReplies"),
|
||||
showInvite: () => this.sendWidgetAction("showInvite"),
|
||||
removeAllowedGroup: (group) =>
|
||||
this.sendWidgetAction("removeAllowedGroup", group),
|
||||
removeAllowedUser: (user) =>
|
||||
this.sendWidgetAction("removeAllowedUser", user),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("post-article", {
|
||||
@ -864,6 +830,11 @@ createWidget("post-article", {
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
if (attrs.showTopicMap) {
|
||||
rows.push(this.buildTopicMap(attrs));
|
||||
}
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
@ -928,6 +899,33 @@ createWidget("post-article", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
buildTopicMap(attrs) {
|
||||
return new RenderGlimmer(
|
||||
this,
|
||||
"div.topic-map.--op",
|
||||
hbs`<TopicMap
|
||||
@model={{@data.model}}
|
||||
@topicDetails={{@data.topicDetails}}
|
||||
@postStream={{@data.postStream}}
|
||||
@showPMMap={{@data.showPMMap}}
|
||||
@showInvite={{@data.showInvite}}
|
||||
@removeAllowedGroup={{@data.removeAllowedGroup}}
|
||||
@removeAllowedUser={{@data.removeAllowedUser}}
|
||||
/>`,
|
||||
{
|
||||
model: attrs.topic,
|
||||
topicDetails: attrs.topic.get("details"),
|
||||
postStream: attrs.topic.postStream,
|
||||
showPMMap: attrs.showPMMap,
|
||||
showInvite: () => this.sendWidgetAction("showInvite"),
|
||||
removeAllowedGroup: (group) =>
|
||||
this.sendWidgetAction("removeAllowedGroup", group),
|
||||
removeAllowedUser: (user) =>
|
||||
this.sendWidgetAction("removeAllowedUser", user),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
let addPostClassesCallbacks = null;
|
||||
|
@ -91,7 +91,9 @@ acceptance("Personal Message - invite", function (needs) {
|
||||
test("can open invite modal", async function (assert) {
|
||||
await visit("/t/pm-for-testing/12");
|
||||
await click(".add-remove-participant-btn");
|
||||
await click(".private-message-map .controls .add-participant-btn");
|
||||
await click(
|
||||
".topic-map__private-message-map .controls .add-participant-btn"
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".d-modal.add-pm-participants .invite-user-control")
|
||||
@ -101,7 +103,9 @@ acceptance("Personal Message - invite", function (needs) {
|
||||
test("shows errors correctly", async function (assert) {
|
||||
await visit("/t/pm-for-testing/12");
|
||||
await click(".add-remove-participant-btn");
|
||||
await click(".private-message-map .controls .add-participant-btn");
|
||||
await click(
|
||||
".topic-map__private-message-map .controls .add-participant-btn"
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".d-modal.add-pm-participants .invite-user-control")
|
||||
|
@ -661,7 +661,8 @@ acceptance("Topic stats update automatically", function () {
|
||||
test("Likes count updates automatically", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const likesCountSelectors = "#post_1 .topic-map .likes .number";
|
||||
const likesCountSelectors =
|
||||
"#post_1 .topic-map .topic-map__likes-trigger .number";
|
||||
const oldLikesCount = query(likesCountSelectors).textContent;
|
||||
const likesChangedFixture = {
|
||||
id: 280,
|
||||
@ -680,87 +681,4 @@ acceptance("Topic stats update automatically", function () {
|
||||
"it updates the likes count on the topic stats"
|
||||
);
|
||||
});
|
||||
|
||||
const postsChangedFixture = {
|
||||
id: 280,
|
||||
type: "stats",
|
||||
posts_count: 999,
|
||||
last_posted_at: "2022-06-20T21:01:45.844Z",
|
||||
last_poster: {
|
||||
id: 1,
|
||||
username: "test",
|
||||
name: "Mr. Tester",
|
||||
avatar_template: "/images/d-logo-sketch-small.png",
|
||||
},
|
||||
};
|
||||
|
||||
test("Replies count updates automatically", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const repliesCountSelectors = "#post_1 .topic-map .replies .number";
|
||||
const oldRepliesCount = query(repliesCountSelectors).textContent;
|
||||
const expectedRepliesCount = (
|
||||
postsChangedFixture.posts_count - 1
|
||||
).toString();
|
||||
|
||||
// simulate the topic posts_count being changed
|
||||
await publishToMessageBus("/topic/280", postsChangedFixture);
|
||||
|
||||
assert.dom(repliesCountSelectors).hasText(expectedRepliesCount);
|
||||
assert.notEqual(
|
||||
oldRepliesCount,
|
||||
expectedRepliesCount,
|
||||
"it updates the replies count on the topic stats"
|
||||
);
|
||||
});
|
||||
|
||||
test("Last replier avatar updates automatically", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
const avatarSelectors = "#post_1 .topic-map .last-reply .avatar";
|
||||
const avatarImg = query(avatarSelectors);
|
||||
|
||||
const oldAvatarTitle = avatarImg.title;
|
||||
const oldAvatarSrc = avatarImg.src;
|
||||
const expectedAvatarTitle = postsChangedFixture.last_poster.name;
|
||||
const expectedAvatarSrc = postsChangedFixture.last_poster.avatar_template;
|
||||
|
||||
// simulate the topic posts_count being changed
|
||||
await publishToMessageBus("/topic/280", postsChangedFixture);
|
||||
|
||||
assert.dom(avatarSelectors).hasAttribute("title", expectedAvatarTitle);
|
||||
assert.notEqual(
|
||||
oldAvatarTitle,
|
||||
expectedAvatarTitle,
|
||||
"it updates the last poster avatar title on the topic stats"
|
||||
);
|
||||
|
||||
assert.dom(avatarSelectors).hasAttribute("src", expectedAvatarSrc);
|
||||
assert.notEqual(
|
||||
oldAvatarSrc,
|
||||
expectedAvatarSrc,
|
||||
"it updates the last poster avatar src on the topic stats"
|
||||
);
|
||||
});
|
||||
|
||||
test("Last replied at updates automatically", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const lastRepliedAtSelectors =
|
||||
"#post_1 .topic-map .last-reply .relative-date";
|
||||
const lastRepliedAtDisplay = query(lastRepliedAtSelectors);
|
||||
const oldTime = lastRepliedAtDisplay.dataset.time;
|
||||
const expectedTime = Date.parse(
|
||||
postsChangedFixture.last_posted_at
|
||||
).toString();
|
||||
|
||||
// simulate the topic posts_count being changed
|
||||
await publishToMessageBus("/topic/280", postsChangedFixture);
|
||||
|
||||
assert.dom(lastRepliedAtSelectors).hasAttribute("data-time", expectedTime);
|
||||
assert.notEqual(
|
||||
oldTime,
|
||||
expectedTime,
|
||||
"it updates the last posted time on the topic stats"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ acceptance("User Card", function (needs) {
|
||||
|
||||
test("opens and closes properly", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-map__users-trigger");
|
||||
await click('a[data-user-card="charlie"]');
|
||||
|
||||
assert.dom(".user-card .card-content").exists();
|
||||
@ -32,6 +33,7 @@ acceptance("User Card - Show Local Time", function (needs) {
|
||||
currentUser.user_option.timezone = "Australia/Brisbane";
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-map__users-trigger");
|
||||
await click('a[data-user-card="charlie"]');
|
||||
|
||||
assert
|
||||
@ -98,6 +100,8 @@ acceptance("User Card - User Status", function (needs) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(".topic-map__users-trigger");
|
||||
await click('a[data-user-card="charlie"]');
|
||||
|
||||
assert.dom(".user-card .user-status").exists();
|
||||
@ -107,6 +111,8 @@ acceptance("User Card - User Status", function (needs) {
|
||||
this.siteSettings.enable_user_status = false;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(".topic-map__users-trigger");
|
||||
await click('a[data-user-card="charlie"]');
|
||||
|
||||
assert.dom(".user-card .user-status").doesNotExist();
|
||||
|
@ -779,9 +779,13 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
assert.dom(".topic-map").doesNotExist();
|
||||
});
|
||||
|
||||
test("topic map - few posts", async function (assert) {
|
||||
test("topic map - few participants", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
posts_count: 10,
|
||||
participant_count: 2,
|
||||
});
|
||||
topic.details.set("participants", [
|
||||
{ username: "eviltrout" },
|
||||
{ username: "codinghorror" },
|
||||
@ -789,25 +793,28 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", {
|
||||
topic,
|
||||
showTopicMap: true,
|
||||
topicPostsCount: 2,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
assert.dom("li.avatars a.poster").doesNotExist();
|
||||
|
||||
await click("nav.buttons button");
|
||||
assert.dom(".topic-map-expanded a.poster").exists({ count: 2 });
|
||||
assert.dom(".topic-map__users-trigger").doesNotExist();
|
||||
assert.dom(".topic-map__users-list a.poster").exists({ count: 2 });
|
||||
});
|
||||
|
||||
test("topic map - participants", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123, posts_count: 10 });
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
posts_count: 10,
|
||||
participant_count: 6,
|
||||
});
|
||||
topic.postStream.setProperties({ userFilters: ["sam", "codinghorror"] });
|
||||
topic.details.set("participants", [
|
||||
{ username: "eviltrout" },
|
||||
{ username: "codinghorror" },
|
||||
{ username: "sam" },
|
||||
{ username: "ZogStrIP" },
|
||||
{ username: "zogstrip" },
|
||||
{ username: "joffreyjaffeux" },
|
||||
{ username: "david" },
|
||||
]);
|
||||
|
||||
this.set("args", {
|
||||
@ -816,12 +823,12 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
assert.dom("li.avatars a.poster").exists({ count: 3 });
|
||||
assert.dom(".topic-map__users-list a.poster").exists({ count: 5 });
|
||||
|
||||
await click("nav.buttons button");
|
||||
assert.dom("li.avatars a.poster").doesNotExist();
|
||||
assert.dom(".topic-map-expanded a.poster").exists({ count: 4 });
|
||||
assert.dom("a.poster.toggled").exists({ count: 2 });
|
||||
await click(".topic-map__users-trigger");
|
||||
assert
|
||||
.dom(".topic-map__users-content .topic-map__users-list a.poster")
|
||||
.exists({ count: 6 });
|
||||
});
|
||||
|
||||
test("topic map - links", async function (assert) {
|
||||
@ -840,17 +847,12 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists({ count: 1 });
|
||||
assert.dom(".map.map-collapsed").exists({ count: 1 });
|
||||
assert.dom(".topic-map-expanded").doesNotExist();
|
||||
|
||||
await click("nav.buttons button");
|
||||
assert.dom(".map.map-collapsed").doesNotExist();
|
||||
assert.dom(".topic-map .d-icon-chevron-up").exists({ count: 1 });
|
||||
assert.dom(".topic-map-expanded").exists({ count: 1 });
|
||||
assert.dom(".topic-map-expanded .topic-link").exists({ count: 5 });
|
||||
|
||||
assert.dom(".topic-map__links-content").doesNotExist();
|
||||
await click(".topic-map__links-trigger");
|
||||
assert.dom(".topic-map__links-content").exists({ count: 1 });
|
||||
assert.dom(".topic-map__links-content .topic-link").exists({ count: 5 });
|
||||
await click(".link-summary button");
|
||||
assert.dom(".topic-map-expanded .topic-link").exists({ count: 6 });
|
||||
assert.dom(".topic-map__links-content .topic-link").exists({ count: 6 });
|
||||
});
|
||||
|
||||
test("topic map - no top reply summary", async function (assert) {
|
||||
@ -860,23 +862,17 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".toggle-summary .top-replies").doesNotExist();
|
||||
assert.dom(".summarization-buttons .top-replies").doesNotExist();
|
||||
});
|
||||
|
||||
test("topic map - has top replies summary", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123, has_summary: true });
|
||||
this.set("args", { topic, showTopicMap: true });
|
||||
this.set("showTopReplies", () => (this.summaryToggled = true));
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} @showTopReplies={{this.showTopReplies}} />`
|
||||
);
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".toggle-summary").exists({ count: 1 });
|
||||
|
||||
await click(".toggle-summary button");
|
||||
assert.ok(this.summaryToggled);
|
||||
assert.dom(".summarization-buttons .top-replies").exists({ count: 1 });
|
||||
});
|
||||
|
||||
test("pm map", async function (assert) {
|
||||
@ -893,8 +889,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".private-message-map").exists({ count: 1 });
|
||||
assert.dom(".private-message-map .user").exists({ count: 1 });
|
||||
assert.dom(".topic-map__private-message-map").exists({ count: 1 });
|
||||
assert.dom(".topic-map__private-message-map .user").exists({ count: 1 });
|
||||
});
|
||||
|
||||
test("post notice - with username", async function (assert) {
|
||||
|
@ -82,18 +82,23 @@
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
margin-left: calc(var(--pm-padding) * -1);
|
||||
border: none;
|
||||
border-radius: var(--pm-border-radius);
|
||||
padding: var(--pm-padding);
|
||||
|
||||
padding-block: var(--pm-padding);
|
||||
padding-left: calc(
|
||||
48px - var(--pm-padding)
|
||||
); // 48px is the width of the avatar
|
||||
section {
|
||||
border: none;
|
||||
background: var(--primary-very-low);
|
||||
padding-inline: var(--pm-padding);
|
||||
}
|
||||
.map:not(.map-collapsed) {
|
||||
.avatars {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.map {
|
||||
padding-top: var(--pm-padding);
|
||||
}
|
||||
|
||||
&__private-message-map {
|
||||
padding-bottom: var(--pm-padding);
|
||||
}
|
||||
|
||||
.participants {
|
||||
|
@ -695,195 +695,9 @@ aside.quote {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
background: var(--primary-very-low);
|
||||
border: 1px solid var(--primary-low);
|
||||
border-top: none; // would cause double top border
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 2.15em;
|
||||
height: 2.15em;
|
||||
}
|
||||
.post-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 100px;
|
||||
padding: 0.15em 0.4em 0.2em;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: var(--font-down-2);
|
||||
line-height: var(--line-height-small);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 0.33em;
|
||||
}
|
||||
|
||||
section {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
width: 100%;
|
||||
margin-bottom: 0.33em;
|
||||
color: var(--primary);
|
||||
line-height: var(--line-height-large);
|
||||
font-weight: normal;
|
||||
font-size: var(--font-0);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1px 0 2px 0;
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
font-weight: normal;
|
||||
font-size: var(--font-down-1);
|
||||
line-height: var(--line-height-small);
|
||||
}
|
||||
|
||||
span.domain {
|
||||
font-size: var(--font-down-2);
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
padding: 1px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.topic-links {
|
||||
tbody {
|
||||
border: none;
|
||||
}
|
||||
tr {
|
||||
border: none;
|
||||
}
|
||||
.badge-notification {
|
||||
margin: 1px 5px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.link-summary .btn {
|
||||
width: 100%;
|
||||
.d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
margin-right: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
& + .controls {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
&.hide-names .user {
|
||||
.username,
|
||||
.group-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
@include ellipsis;
|
||||
border: 1px solid var(--primary-low);
|
||||
border-radius: 0.25em;
|
||||
padding: 0;
|
||||
margin: 0.125em 0.25em 0.125em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
|
||||
.user-link,
|
||||
.group-link {
|
||||
@include ellipsis;
|
||||
color: var(--primary-high);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.d-icon-users {
|
||||
margin-left: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.username,
|
||||
.group-name {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.remove-invited {
|
||||
display: flex;
|
||||
flex: 1 0 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
height: calc(100% + 0.25em);
|
||||
margin-right: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-remove-participant-btn {
|
||||
.d-icon {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-avatar,
|
||||
.avatar-flair-preview,
|
||||
.user-card-avatar,
|
||||
.topic-map .poster,
|
||||
.user-profile-avatar,
|
||||
.user-image,
|
||||
.latest-topic-list-item {
|
||||
@ -932,38 +746,6 @@ aside.quote {
|
||||
font-size: var(--font-up-4);
|
||||
}
|
||||
}
|
||||
.topic-map .poster .avatar-flair {
|
||||
right: 0;
|
||||
background-size: 12px 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -3px;
|
||||
color: var(--primary);
|
||||
&.rounded {
|
||||
background-size: 12px 12px;
|
||||
border-radius: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -2px;
|
||||
right: 0;
|
||||
}
|
||||
.d-icon {
|
||||
font-size: var(--font-down-2);
|
||||
}
|
||||
}
|
||||
|
||||
.map {
|
||||
&:first-of-type {
|
||||
display: flex;
|
||||
.buttons {
|
||||
margin-left: auto;
|
||||
order: 15;
|
||||
.btn {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-body {
|
||||
// this is necessary for ANYTHING that extends past the right edge of
|
||||
|
@ -88,7 +88,7 @@
|
||||
grid-template-columns: auto;
|
||||
.topic-navigation {
|
||||
grid-area: posts;
|
||||
grid-row: 2;
|
||||
grid-row: 3;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
@import "tap-tile";
|
||||
@import "time-input";
|
||||
@import "time-shortcut-picker";
|
||||
@import "topic-map";
|
||||
@import "topic-query-filter";
|
||||
@import "user-card";
|
||||
@import "user-info";
|
||||
|
500
app/assets/stylesheets/common/components/topic-map.scss
Normal file
500
app/assets/stylesheets/common/components/topic-map.scss
Normal file
@ -0,0 +1,500 @@
|
||||
article {
|
||||
// topic map under OP
|
||||
.topic-map {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container.posts {
|
||||
// topic map at bottom of topic
|
||||
> .topic-map {
|
||||
grid-area: posts;
|
||||
grid-row: 2;
|
||||
max-width: calc(
|
||||
var(--topic-avatar-width) + var(--topic-body-width) +
|
||||
(var(--topic-body-width-padding) * 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
// both topic maps
|
||||
--chart-line-color: var(--tertiary);
|
||||
--chart-point-color: var(--tertiary-medium);
|
||||
--chart-prediction-color: var(--primary-low-mid);
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
box-sizing: border-box;
|
||||
max-width: calc(
|
||||
var(--topic-avatar-width) + var(--topic-body-width) +
|
||||
(var(--topic-body-width-padding) * 2)
|
||||
);
|
||||
|
||||
@include breakpoint(mobile-large) {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
|
||||
.--users-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 2em;
|
||||
align-self: center;
|
||||
flex: 1 2 0;
|
||||
gap: 0.25em;
|
||||
|
||||
.avatar {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.avatar-flair,
|
||||
.post-count {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.fk-d-menu__content {
|
||||
.fk-d-menu__inner-content,
|
||||
.d-modal__container {
|
||||
box-sizing: border-box;
|
||||
max-height: 80dvh;
|
||||
min-width: 20em;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
align-items: start;
|
||||
overscroll-behavior: contain;
|
||||
padding: 1.5em;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
.desktop-view & {
|
||||
@include breakpoint(mobile-large) {
|
||||
min-width: unset;
|
||||
max-width: 90dvw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
font-size: var(--font-up-1);
|
||||
margin-top: -0.35em;
|
||||
margin-bottom: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__contents {
|
||||
padding: 0.5em 0 0.5em
|
||||
calc(var(--topic-body-width-padding) + var(--topic-avatar-width));
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: var(--font-up-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.topic-map__stats {
|
||||
gap: 0.75em;
|
||||
|
||||
&.--single-stat {
|
||||
button {
|
||||
flex-direction: row;
|
||||
gap: 0.25em;
|
||||
|
||||
span {
|
||||
font-size: var(--font-0);
|
||||
color: var(--primary-700) !important;
|
||||
min-width: unset;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fk-d-menu__trigger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
|
||||
.number {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
|
||||
.topic-map__stat-label {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
width: 100%;
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__additional-contents {
|
||||
padding-left: calc(
|
||||
var(--topic-body-width-padding) + var(--topic-avatar-width)
|
||||
);
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
margin-right: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
// PMs
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
& + .controls {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
&.hide-names .user {
|
||||
.username,
|
||||
.group-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
@include ellipsis;
|
||||
border: 1px solid var(--primary-low);
|
||||
border-radius: 0.25em;
|
||||
padding: 0;
|
||||
margin: 0.125em 0.25em 0.125em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
|
||||
.user-link,
|
||||
.group-link {
|
||||
@include ellipsis;
|
||||
color: var(--primary-high);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.d-icon-users {
|
||||
margin-left: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.username,
|
||||
.group-name {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.remove-invited {
|
||||
display: flex;
|
||||
flex: 1 0 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
height: calc(100% + 0.25em);
|
||||
margin-right: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-remove-participant-btn {
|
||||
.d-icon {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.view-explainer {
|
||||
color: var(--primary-700);
|
||||
font-size: var(--font-down-1);
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.estimated-read-time {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: end;
|
||||
line-height: 1.2;
|
||||
color: var(--primary-high);
|
||||
white-space: nowrap;
|
||||
|
||||
span:first-child {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 475px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-view {
|
||||
.d-modal[class*="topic-map__"] {
|
||||
.d-modal__body {
|
||||
padding: 1em 1em 2em 1em;
|
||||
|
||||
h3 {
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-up-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-owner .onscreen-post {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// DMenu popups
|
||||
|
||||
.topic-map__likes-content {
|
||||
.fk-d-menu__inner-content,
|
||||
.d-modal__body {
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
li > a {
|
||||
display: grid;
|
||||
grid-template-areas: "user likes" "post post";
|
||||
grid-template-columns: auto 1fr;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
padding: 1em 0;
|
||||
gap: 0.25em;
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
background: var(--primary-very-low);
|
||||
box-shadow: -1em 0px 0px 0px var(--primary-very-low),
|
||||
1em 0px 0px 0px var(--primary-very-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.like-section__user {
|
||||
grid-area: user;
|
||||
|
||||
color: var(--primary-high);
|
||||
justify-content: start;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
font-weight: bold;
|
||||
gap: 0.5em;
|
||||
img {
|
||||
position: relative;
|
||||
top: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.like-section__likes {
|
||||
grid-area: likes;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
|
||||
gap: 0.25em;
|
||||
color: var(--primary-medium);
|
||||
justify-content: end;
|
||||
font-size: var(--font-0);
|
||||
.d-icon {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--love);
|
||||
position: relative;
|
||||
top: 0.28em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
grid-area: post;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--primary-high);
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map__users-content {
|
||||
.fk-d-menu__inner-content,
|
||||
.d-modal__body {
|
||||
.topic-map__users-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.poster {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 2.25em;
|
||||
height: 2.25em;
|
||||
}
|
||||
}
|
||||
.post-count,
|
||||
.avatar-flair {
|
||||
position: absolute;
|
||||
border-radius: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.post-count {
|
||||
top: -0.15em;
|
||||
right: -0.25em;
|
||||
padding: 0.15em 0.4em 0.2em;
|
||||
font-size: var(--font-down-2);
|
||||
line-height: var(--line-height-small);
|
||||
}
|
||||
.avatar-flair {
|
||||
right: -0.25em;
|
||||
bottom: -0.15em;
|
||||
font-size: var(--font-down-3);
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map__links-content {
|
||||
.fk-d-menu__inner-content,
|
||||
.d-modal__body {
|
||||
.topic-links {
|
||||
width: 100%;
|
||||
|
||||
tbody {
|
||||
border: none;
|
||||
}
|
||||
tr {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
border-bottom: none;
|
||||
td:nth-of-type(2) {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
span.domain {
|
||||
font-size: var(--font-down-2);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
.link-summary .btn {
|
||||
width: 100%;
|
||||
.d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map__views-content {
|
||||
.fk-d-menu__inner-content,
|
||||
.d-modal__body {
|
||||
.topic-views {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
padding: 0.5em 1em 0;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: space-between;
|
||||
}
|
||||
&__count {
|
||||
font-size: var(--font-up-4);
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.topic-views) {
|
||||
min-width: unset;
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -375,76 +375,6 @@ pre.codeblock-buttons:hover {
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
margin: 20px 0 20px var(--topic-body-width-padding);
|
||||
.map {
|
||||
.secondary {
|
||||
text-align: center;
|
||||
}
|
||||
li {
|
||||
float: left;
|
||||
padding: 7px 10px;
|
||||
&:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.number {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
.number,
|
||||
.d-icon {
|
||||
font-size: var(--font-up-2);
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
button .d-icon,
|
||||
button:hover .d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
.avatar a {
|
||||
float: left;
|
||||
}
|
||||
.topic-map-post {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
.avatars,
|
||||
.links,
|
||||
.information {
|
||||
padding: 7px 10px 7px 10px;
|
||||
color: var(--primary);
|
||||
}
|
||||
.buttons {
|
||||
float: right;
|
||||
.btn {
|
||||
border: 0;
|
||||
padding: 0 23px;
|
||||
color: var(--primary-med-or-secondary-high);
|
||||
background: var(--blend-primary-secondary-5);
|
||||
border-left: 1px solid var(--primary-low);
|
||||
border-top: 1px solid transparent;
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
background: var(--primary-low);
|
||||
}
|
||||
&.collapsed {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.fa {
|
||||
margin: 0;
|
||||
font-size: var(--font-up-2);
|
||||
line-height: 52px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-summary .summary-box {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#topic-footer-buttons {
|
||||
max-width: calc(
|
||||
var(--topic-body-width) + (var(--topic-body-width-padding) * 2) +
|
||||
|
@ -243,77 +243,6 @@ a.reply-to-tab {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
margin: 10px 0;
|
||||
h4 {
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
.user {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.map-collapsed {
|
||||
.secondary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.map {
|
||||
li {
|
||||
float: left;
|
||||
padding: 7px 8px;
|
||||
&:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
.number {
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
.number,
|
||||
.d-icon {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
.avatar + a {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
li.avatars {
|
||||
display: none;
|
||||
}
|
||||
.links,
|
||||
.information,
|
||||
.avatars {
|
||||
padding: 10px;
|
||||
color: var(--primary);
|
||||
overflow: auto;
|
||||
}
|
||||
.information {
|
||||
p {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
.btn {
|
||||
border: 0;
|
||||
padding: 0 15px;
|
||||
color: var(--primary-med-or-secondary-high);
|
||||
background: var(--blend-primary-secondary-5);
|
||||
border-left: 1px solid var(--primary-low);
|
||||
.fa {
|
||||
margin: 0;
|
||||
font-size: var(--font-up-2);
|
||||
line-height: 52px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-summary {
|
||||
.summarization-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#topic-footer-buttons {
|
||||
.d-icon-bookmark.bookmarked,
|
||||
.d-icon-discourse-bookmark-clock.bookmarked {
|
||||
|
@ -136,7 +136,7 @@ sub sub {
|
||||
}
|
||||
|
||||
.container.posts .topic-navigation {
|
||||
// better positioning for the docked progress bar on large screens using mobile view
|
||||
// docked progress bar on large screens using mobile view
|
||||
grid-area: posts;
|
||||
grid-row: 2;
|
||||
grid-row: 3; // after topic map
|
||||
}
|
||||
|
@ -171,10 +171,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.topic-map h4 {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.quote-controls,
|
||||
.quote-controls .d-icon {
|
||||
color: var(--primary-medium);
|
||||
@ -203,10 +199,6 @@ html {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Post controls
|
||||
|
||||
.topic-admin-menu-button-container,
|
||||
|
@ -2237,6 +2237,7 @@ en:
|
||||
enable: "Show top replies"
|
||||
disable: "Show All Posts"
|
||||
short_label: "Top replies"
|
||||
show_all_label: "Show all"
|
||||
short_title: "Show this topic top replies: the most interesting posts as determined by the community"
|
||||
|
||||
deleted_filter:
|
||||
@ -4135,6 +4136,17 @@ en:
|
||||
clicks:
|
||||
one: "%{count} click"
|
||||
other: "%{count} clicks"
|
||||
menu_titles:
|
||||
replies: Most liked replies
|
||||
views: Recent views
|
||||
view_explainer: One view per unique visitor every 8 hours.
|
||||
no_views: No view stats yet, check back later.
|
||||
chart_error: Error rendering chart, please try again.
|
||||
views: "Views"
|
||||
predicted_views: "Predicted Views"
|
||||
so_far: (so far)
|
||||
read: read
|
||||
minutes: min
|
||||
post_links:
|
||||
about: "expand more links for this post"
|
||||
title:
|
||||
|
@ -3,7 +3,8 @@
|
||||
module PageObjects
|
||||
module Components
|
||||
class PrivateMessageMap < PageObjects::Components::Base
|
||||
PRIVATE_MESSAGE_MAP_KLASS = ".private-message-map"
|
||||
PRIVATE_MESSAGE_MAP_KLASS = ".topic-map__private-message-map"
|
||||
|
||||
def is_visible?
|
||||
has_css?(PRIVATE_MESSAGE_MAP_KLASS)
|
||||
end
|
||||
|
@ -3,7 +3,7 @@
|
||||
module PageObjects
|
||||
module Components
|
||||
class TopicMap < PageObjects::Components::Base
|
||||
TOPIC_MAP_KLASS = ".topic-map"
|
||||
TOPIC_MAP_KLASS = ".topic-map.--op"
|
||||
|
||||
def is_visible?
|
||||
has_css?(TOPIC_MAP_KLASS)
|
||||
@ -13,68 +13,53 @@ module PageObjects
|
||||
has_no_css?(TOPIC_MAP_KLASS)
|
||||
end
|
||||
|
||||
def is_collapsed?
|
||||
has_css?("#{TOPIC_MAP_KLASS} .map-collapsed")
|
||||
end
|
||||
|
||||
def expand
|
||||
find("#{TOPIC_MAP_KLASS} .map-collapsed .btn").click if is_collapsed?
|
||||
def has_no_users?
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .topic-map__users-trigger")
|
||||
end
|
||||
|
||||
def has_no_likes?
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .likes")
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .topic-map__likes-trigger")
|
||||
end
|
||||
|
||||
def has_no_links?
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .links")
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .topic-map__links-trigger")
|
||||
end
|
||||
|
||||
def users_count
|
||||
find("#{TOPIC_MAP_KLASS} .users .number").text.to_i
|
||||
end
|
||||
|
||||
def replies_count
|
||||
find("#{TOPIC_MAP_KLASS} .replies .number").text.to_i
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__users-trigger .number").text.to_i
|
||||
end
|
||||
|
||||
def likes_count
|
||||
find("#{TOPIC_MAP_KLASS} .likes .number").text.to_i
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__likes-trigger .number").text.to_i
|
||||
end
|
||||
|
||||
def links_count
|
||||
find("#{TOPIC_MAP_KLASS} .links .number").text.to_i
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__links-trigger .number").text.to_i
|
||||
end
|
||||
|
||||
def views_count
|
||||
find("#{TOPIC_MAP_KLASS} .views .number").text.to_i
|
||||
end
|
||||
|
||||
def created_details
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map-post.created-at")
|
||||
end
|
||||
|
||||
def created_relative_date
|
||||
created_details.find(".relative-date").text
|
||||
end
|
||||
|
||||
def last_reply_details
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map-post.last-reply")
|
||||
end
|
||||
|
||||
def last_reply_relative_date
|
||||
last_reply_details.find(".relative-date").text
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__views-trigger .number").text.to_i
|
||||
end
|
||||
|
||||
def avatars_details
|
||||
find("#{TOPIC_MAP_KLASS} .map .avatars").all(".poster.trigger-user-card")
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__users-list").all(".poster.trigger-user-card")
|
||||
end
|
||||
|
||||
def expanded_map_avatars_details
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map-expanded .avatars").all(".poster.trigger-user-card")
|
||||
def expanded_avatars_details
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__users-trigger").click
|
||||
find("#{TOPIC_MAP_KLASS} .topic-map__users-content").all(".poster.trigger-user-card")
|
||||
end
|
||||
|
||||
def has_no_avatars_details_in_map?
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .map .avatars")
|
||||
has_no_css?("#{TOPIC_MAP_KLASS} .topic-map__users-list")
|
||||
end
|
||||
|
||||
def has_bottom_map?
|
||||
has_css?(".topic-map.--bottom")
|
||||
end
|
||||
|
||||
def has_no_bottom_map?
|
||||
has_no_css?(".topic-map.--bottom")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,7 +2,11 @@
|
||||
|
||||
describe "Topic Map - Private Message", type: :system do
|
||||
fab!(:user) { Fabricate(:admin, refresh_auto_groups: true) }
|
||||
fab!(:other_user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:other_user_1) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:other_user_2) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:other_user_3) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:other_user_4) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:other_user_5) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:last_post_user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:topic) do
|
||||
Fabricate(
|
||||
@ -11,7 +15,11 @@ describe "Topic Map - Private Message", type: :system do
|
||||
user: user,
|
||||
topic_allowed_users: [
|
||||
Fabricate.build(:topic_allowed_user, user: user),
|
||||
Fabricate.build(:topic_allowed_user, user: other_user),
|
||||
Fabricate.build(:topic_allowed_user, user: other_user_1),
|
||||
Fabricate.build(:topic_allowed_user, user: other_user_2),
|
||||
Fabricate.build(:topic_allowed_user, user: other_user_3),
|
||||
Fabricate.build(:topic_allowed_user, user: other_user_4),
|
||||
Fabricate.build(:topic_allowed_user, user: other_user_5),
|
||||
Fabricate.build(:topic_allowed_user, user: last_post_user),
|
||||
],
|
||||
)
|
||||
@ -38,41 +46,35 @@ describe "Topic Map - Private Message", type: :system do
|
||||
# topic map appears after OP
|
||||
expect(topic_page).to have_topic_map
|
||||
|
||||
# created avatar display
|
||||
expect(topic_map.created_details).to have_selector("img[src=\"#{avatar_url(user, 24)}\"]")
|
||||
expect(topic_map.created_relative_date).to eq "1d"
|
||||
# user count
|
||||
expect(topic_map).to have_no_users
|
||||
[other_user_1, other_user_2, other_user_3, other_user_4, other_user_5].each do |usr|
|
||||
Fabricate(:post, topic: topic, user: usr, created_at: 1.day.ago)
|
||||
end
|
||||
page.refresh
|
||||
expect(topic_map.users_count).to eq 6
|
||||
|
||||
# replies, user count
|
||||
expect {
|
||||
Fabricate(:post, topic: topic, user: user, created_at: 1.day.ago)
|
||||
sign_in(last_post_user)
|
||||
topic_page.visit_topic_and_open_composer(topic)
|
||||
topic_page.send_reply("this is a cool-cat post") # fabricating posts doesn't update the last post details
|
||||
topic_page.visit_topic(topic)
|
||||
}.to change(topic_map, :replies_count).by(2).and change(topic_map, :users_count).by(1)
|
||||
|
||||
#last reply avatar display
|
||||
expect(topic_map.last_reply_details).to have_selector(
|
||||
"img[src=\"#{avatar_url(last_post_user, 24)}\"]",
|
||||
)
|
||||
expect(topic_map.last_reply_relative_date).to eq "1m"
|
||||
}.to change(topic_map, :users_count).by(1)
|
||||
|
||||
# avatars details with post counts
|
||||
2.times { Fabricate(:post, topic: topic) }
|
||||
Fabricate(:post, user: user, topic: topic)
|
||||
2.times { Fabricate(:post, user: user, topic: topic) }
|
||||
Fabricate(:post, user: last_post_user, topic: topic)
|
||||
page.refresh
|
||||
avatars = topic_map.avatars_details
|
||||
expect(avatars.length).to eq 3 # max no. of avatars in a collapsed map
|
||||
expect(avatars[0]).to have_selector("img[src=\"#{avatar_url(user, 48)}\"]")
|
||||
expect(avatars[0].find(".post-count").text).to eq "3"
|
||||
expect(avatars[1]).to have_selector("img[src=\"#{avatar_url(last_post_user, 48)}\"]")
|
||||
expect(avatars[1].find(".post-count").text).to eq "2"
|
||||
expect(avatars[2]).to have_no_css(".post-count")
|
||||
expect(avatars.length).to eq 5 # max no. of avatars in a collapsed map
|
||||
|
||||
topic_map.expand
|
||||
expect(topic_map).to have_no_avatars_details_in_map
|
||||
expect(topic_map.expanded_map_avatars_details.length).to eq 4
|
||||
expanded_avatars = topic_map.expanded_avatars_details
|
||||
expect(expanded_avatars[0]).to have_selector("img[src=\"#{avatar_url(user, 48)}\"]")
|
||||
expect(expanded_avatars[0].find(".post-count").text).to eq "3"
|
||||
expect(expanded_avatars[1]).to have_selector("img[src=\"#{avatar_url(last_post_user, 48)}\"]")
|
||||
expect(expanded_avatars[1].find(".post-count").text).to eq "2"
|
||||
expect(expanded_avatars[2]).to have_no_css(".post-count")
|
||||
expect(expanded_avatars.length).to eq 7
|
||||
|
||||
# views count
|
||||
# TODO (martin) Investigate flakiness
|
||||
@ -84,8 +86,11 @@ describe "Topic Map - Private Message", type: :system do
|
||||
|
||||
# likes count
|
||||
expect(topic_map).to have_no_likes
|
||||
Fabricate(:post, topic: topic, like_count: 5)
|
||||
page.refresh
|
||||
expect(topic_map).to have_no_likes
|
||||
topic_page.click_like_reaction_for(original_post)
|
||||
expect(topic_map.likes_count).to eq 1
|
||||
expect(topic_map.likes_count).to eq 6
|
||||
end
|
||||
|
||||
it "has private message map that shows correct participants and allows editing of participant invites" do
|
||||
@ -98,7 +103,17 @@ describe "Topic Map - Private Message", type: :system do
|
||||
# participants' links and avatars
|
||||
private_message_map
|
||||
.participants_details
|
||||
.zip([user, other_user, last_post_user]) do |details, usr|
|
||||
.zip(
|
||||
[
|
||||
user,
|
||||
other_user_1,
|
||||
other_user_2,
|
||||
other_user_3,
|
||||
other_user_4,
|
||||
other_user_5,
|
||||
last_post_user,
|
||||
],
|
||||
) do |details, usr|
|
||||
expect(details).to have_link(usr.username, href: "/u/#{usr.username}")
|
||||
expect(details.find(".trigger-user-card")).to have_selector(
|
||||
"img[src=\"#{avatar_url(usr, 24)}\"]",
|
||||
@ -134,7 +149,7 @@ describe "Topic Map - Private Message", type: :system do
|
||||
expect(private_message_map).to have_add_participants_button
|
||||
private_message_map.click_add_participants_button
|
||||
expect(private_message_invite_modal).to be_open
|
||||
private_message_invite_modal.select_invitee(other_user)
|
||||
private_message_invite_modal.select_invitee(other_user_1)
|
||||
private_message_invite_modal.click_primary_button
|
||||
expect(private_message_invite_modal).to have_invitee_already_exists_error
|
||||
private_message_invite_modal.select_invitee(last_post_user)
|
||||
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
describe "Topic Map", type: :system do
|
||||
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:topic) { Fabricate(:topic, user: user, created_at: 1.day.ago) }
|
||||
fab!(:topic) { Fabricate(:topic, user: user, created_at: 2.day.ago) }
|
||||
fab!(:original_post) { Fabricate(:post, topic: topic, user: user, created_at: 1.day.ago) }
|
||||
|
||||
fab!(:other_user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
@ -22,45 +22,46 @@ describe "Topic Map", type: :system do
|
||||
|
||||
# topic map only appears after at least 1 reply
|
||||
expect(topic_page).to have_no_topic_map
|
||||
Fabricate(:post, topic: topic, created_at: 1.day.ago)
|
||||
Fabricate(:post, topic: topic, created_at: 2.day.ago)
|
||||
Fabricate(:post, topic: topic, created_at: 1.day.ago, like_count: 3)
|
||||
2.times { Fabricate(:post, topic: topic, created_at: 1.day.ago, like_count: 1) }
|
||||
page.refresh
|
||||
expect(topic_page).to have_topic_map
|
||||
expect(topic_map).to have_no_users
|
||||
|
||||
# created avatar display
|
||||
expect(topic_map.created_details).to have_selector("img[src=\"#{avatar_url(user, 24)}\"]")
|
||||
expect(topic_map.created_relative_date).to eq "1d"
|
||||
Fabricate(:post, topic: topic, created_at: 1.day.ago)
|
||||
page.refresh
|
||||
expect(topic_map.users_count).to eq 6
|
||||
|
||||
# replies, user count
|
||||
# user count
|
||||
expect {
|
||||
Fabricate(:post, topic: topic, user: user, created_at: 1.day.ago)
|
||||
sign_in(last_post_user)
|
||||
topic_page.visit_topic_and_open_composer(topic)
|
||||
topic_page.send_reply("this is a cool-cat post") # fabricating posts doesn't update the last post details
|
||||
topic_page.visit_topic(topic)
|
||||
}.to change(topic_map, :replies_count).by(2).and change(topic_map, :users_count).by(1)
|
||||
}.to change(topic_map, :users_count).by(1)
|
||||
|
||||
#last reply avatar display
|
||||
expect(topic_map.last_reply_details).to have_selector(
|
||||
"img[src=\"#{avatar_url(last_post_user, 24)}\"]",
|
||||
)
|
||||
expect(topic_map.last_reply_relative_date).to eq "1m"
|
||||
# bottom map, avatars details with post counts
|
||||
expect(topic_map).to have_no_bottom_map
|
||||
|
||||
# avatars details with post counts
|
||||
Fabricate(:post, topic: topic)
|
||||
Fabricate(:post, user: user, topic: topic)
|
||||
Fabricate(:post, user: last_post_user, topic: topic)
|
||||
page.refresh
|
||||
avatars = topic_map.avatars_details
|
||||
expect(avatars.length).to eq 3 # max no. of avatars in a collapsed map
|
||||
expect(avatars[0]).to have_selector("img[src=\"#{avatar_url(user, 48)}\"]")
|
||||
expect(avatars[0].find(".post-count").text).to eq "3"
|
||||
expect(avatars[1]).to have_selector("img[src=\"#{avatar_url(last_post_user, 48)}\"]")
|
||||
expect(avatars[1].find(".post-count").text).to eq "2"
|
||||
expect(avatars[2]).to have_no_css(".post-count")
|
||||
|
||||
topic_map.expand
|
||||
expect(topic_map).to have_no_avatars_details_in_map
|
||||
expect(topic_map.expanded_map_avatars_details.length).to eq 4
|
||||
expect(topic_map).to have_bottom_map
|
||||
|
||||
avatars = topic_map.avatars_details
|
||||
expect(avatars.length).to eq 5 # max no. of avatars in a collapsed map
|
||||
|
||||
expanded_avatars = topic_map.expanded_avatars_details
|
||||
expect(expanded_avatars[0]).to have_selector("img[src=\"#{avatar_url(user, 48)}\"]")
|
||||
expect(expanded_avatars[0].find(".post-count").text).to eq "3"
|
||||
expect(expanded_avatars[1]).to have_selector("img[src=\"#{avatar_url(last_post_user, 48)}\"]")
|
||||
expect(expanded_avatars[1].find(".post-count").text).to eq "2"
|
||||
expect(expanded_avatars[2]).to have_no_css(".post-count")
|
||||
expect(expanded_avatars.length).to eq 8
|
||||
|
||||
# views count
|
||||
# TODO (martin) Investigate flakiness
|
||||
@ -73,6 +74,6 @@ describe "Topic Map", type: :system do
|
||||
# likes count
|
||||
# expect(topic_map).to have_no_likes
|
||||
# topic_page.click_like_reaction_for(original_post)
|
||||
# expect(topic_map.likes_count).to eq 1
|
||||
# expect(topic_map.likes_count).to eq 6
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user