Skip to content

Commit 797a0dd

Browse files
authored
Merge pull request #6603 from WoltLab/63-render-user-reactions
User profile: Unify visuals of tabs "recent activities" and "reactions"
2 parents 03635da + f2c2f9c commit 797a0dd

21 files changed

Lines changed: 593 additions & 604 deletions

File tree

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
{foreach from=$likeList item=like}
2-
<li>
3-
<div class="box48">
1+
{foreach from=$likeList item='like'}
2+
<div class="
3+
recentActivityListItem
4+
recentActivityListItem--withDescription
5+
{if $like->isIgnoredContent()}ignoredUserContent{/if}
6+
">
7+
<div class="recentActivityListItem__avatar">
48
{user object=$like->getUserProfile() type='avatar48' ariaHidden='true' tabindex='-1'}
5-
6-
<div>
7-
<div class="containerHeadline">
8-
<h3>
9-
{user object=$like->getUserProfile()}
10-
<small class="separatorLeft">{time time=$like->time}</small>
11-
</h3>
12-
<div>{unsafe:$like->getTitle()}</div>
13-
<small class="containerContentType">{$like->getObjectTypeDescription()}</small>
14-
</div>
15-
16-
<div class="containerContent">{unsafe:$like->getDescription()}</div>
9+
</div>
10+
11+
<h3 class="recentActivityListItem__title">
12+
{if $like->getLink()}
13+
<a href="{$like->getLink()}" class="recentActivityListItem__link">{unsafe:$like->getTitle()}</a>
14+
{else}
15+
{unsafe:$like->getTitle()}
16+
{/if}
17+
</h3>
18+
19+
{if $like->getDescription()}
20+
<div class="recentActivityListItem__description">
21+
{unsafe:$like->getDescription()}
1722
</div>
23+
{/if}
24+
25+
<div class="recentActivityListItem__time">
26+
{time time=$like->time}
1827
</div>
19-
</li>
28+
</div>
2029
{/foreach}
Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,50 @@
1-
<script data-relocate="true">
2-
require(['WoltLabSuite/Core/Ui/Reaction/Profile/Loader', 'Language'], function(UiReactionProfileLoader, Language) {
3-
Language.addObject({
4-
'wcf.like.reaction.noMoreEntries': '{jslang}wcf.like.reaction.noMoreEntries{/jslang}',
5-
'wcf.like.reaction.more': '{jslang}wcf.like.reaction.more{/jslang}'
6-
});
7-
8-
new UiReactionProfileLoader({$userID});
9-
});
10-
</script>
11-
12-
<ul id="likeList" class="containerList recentActivityList likeList" data-last-like-time="{$lastLikeTime}">
13-
<li class="containerListButtonGroup likeTypeSelection">
14-
<ul class="buttonGroup" id="likeType">
15-
<li><a class="button small active" data-like-type="received">{lang}wcf.like.reactionsReceived{/lang}</a></li>
16-
<li><a class="button small" data-like-type="given">{lang}wcf.like.reactionsGiven{/lang}</a></li>
1+
<div id="reactionList" class="recentActivityList recentActivityList--userProfileContent userProfileReactionList"
2+
data-last-like-time="{$lastLikeTime}"
3+
data-user-id="{$userID}"
4+
data-target-type="received"
5+
data-reaction-type-id="0"
6+
>
7+
<div class="userProfileReactionList__typeSelection">
8+
<ul class="buttonGroup">
9+
<li>
10+
<button type="button" class="button small active" data-target-type="received">
11+
{lang}wcf.like.reactionsReceived{/lang}
12+
</button>
13+
</li>
14+
<li>
15+
<button type="button" class="button small" data-target-type="given">
16+
{lang}wcf.like.reactionsGiven{/lang}
17+
</button>
18+
</li>
1719
</ul>
18-
20+
1921
{if $__wcf->getReactionHandler()->getReactionTypes()|count > 1}
20-
<ul class="buttonGroup" id="reactionType">
22+
<ul class="buttonGroup">
2123
{foreach from=$__wcf->getReactionHandler()->getReactionTypes() item=reactionType name=reactionTypeLoop}
22-
<li><a class="button small jsTooltip" data-reaction-type-id="{$reactionType->reactionTypeID}" title="{$reactionType->getTitle()}" data-is-assignable="{if $reactionType->isAssignable}1{else}0{/if}">{unsafe:$reactionType->renderIcon()} <span class="invisible">{$reactionType->getTitle()}</span></a></li>
24+
<li>
25+
<button
26+
type="button"
27+
class="button small jsTooltip"
28+
data-reaction-type-id="{$reactionType->reactionTypeID}"
29+
title="{$reactionType->getTitle()}"
30+
data-is-assignable="{if $reactionType->isAssignable}1{else}0{/if}"
31+
>
32+
{unsafe:$reactionType->renderIcon()}
33+
</button>
34+
</li>
2335
{/foreach}
2436
</ul>
2537
{/if}
26-
</li>
27-
38+
</div>
39+
2840
{include file='userProfileLikeItem'}
29-
</ul>
41+
</div>
42+
43+
<script data-relocate="true">
44+
require(['WoltLabSuite/Core/Component/User/Reaction/Loader'], ({ setup }) => {
45+
{jsphrase name='wcf.like.reaction.noMoreEntries'}
46+
{jsphrase name='wcf.like.reaction.more'}
47+
48+
setup(document.getElementById('reactionList'));
49+
});
50+
</script>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Loads a paginated list of reactions for a user profile.
3+
*
4+
* @author Marcel Werk
5+
* @copyright 2001-2026 WoltLab GmbH
6+
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7+
* @since 6.3
8+
*/
9+
10+
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
11+
import { ApiResult, apiResultFromError, apiResultFromValue } from "../../Result";
12+
13+
type Response = {
14+
lastLikeTime: number;
15+
template: string;
16+
};
17+
18+
export async function renderUserReactions(
19+
userID: number,
20+
targetType: string,
21+
lastLikeTime: number = 0,
22+
reactionTypeID: number = 0,
23+
): Promise<ApiResult<Response | Record<string, never>>> {
24+
const url = new URL(`${window.WSC_RPC_API_URL}core/users/${userID}/reactions/render`);
25+
url.searchParams.set("targetType", targetType);
26+
url.searchParams.set("lastLikeTime", lastLikeTime.toString());
27+
url.searchParams.set("reactionTypeID", reactionTypeID.toString());
28+
29+
let response: Response | Record<string, never>;
30+
try {
31+
response = (await prepareRequest(url).get().fetchAsJson()) as Response | Record<string, never>;
32+
} catch (e) {
33+
return apiResultFromError(e);
34+
}
35+
36+
return apiResultFromValue(response);
37+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Handles the reaction list in the user profile.
3+
*
4+
* @author Marcel Werk
5+
* @copyright 2001-2026 WoltLab GmbH
6+
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7+
* @since 6.3
8+
*/
9+
10+
import DomUtil from "WoltLabSuite/Core/Dom/Util";
11+
import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
12+
import { getPhrase } from "WoltLabSuite/Core/Language";
13+
import { renderUserReactions } from "WoltLabSuite/Core/Api/Users/Reactions/RenderUserReactions";
14+
15+
async function loadMore(container: HTMLElement): Promise<void> {
16+
const result = await renderUserReactions(
17+
parseInt(container.dataset.userId || "0"),
18+
container.dataset.targetType || "received",
19+
parseInt(container.dataset.lastLikeTime || "0"),
20+
parseInt(container.dataset.reactionTypeId || "0"),
21+
);
22+
const response = result.unwrap();
23+
24+
if ("template" in response) {
25+
container.dataset.lastLikeTime = response.lastLikeTime.toString();
26+
27+
const showMoreButton = container.querySelector(".recentActivityList__showMoreButton")!;
28+
const fragment = DomUtil.createFragmentFromHtml(response.template);
29+
container.insertBefore(fragment, showMoreButton);
30+
31+
showMoreButton.querySelector("button")!.hidden = false;
32+
showMoreButton.querySelector("small")!.hidden = true;
33+
} else {
34+
const showMoreButton = container.querySelector(".recentActivityList__showMoreButton")!;
35+
showMoreButton.querySelector("button")!.hidden = true;
36+
showMoreButton.querySelector("small")!.hidden = false;
37+
}
38+
}
39+
40+
async function reload(container: HTMLElement): Promise<void> {
41+
container.querySelectorAll(":scope > div:not(:first-child):not(:last-child)").forEach((el) => el.remove());
42+
43+
container.dataset.lastLikeTime = "0";
44+
45+
const showMoreButton = container.querySelector(".recentActivityList__showMoreButton");
46+
if (showMoreButton !== null) {
47+
showMoreButton.querySelector("button")!.hidden = false;
48+
showMoreButton.querySelector("small")!.hidden = true;
49+
}
50+
51+
await loadMore(container);
52+
}
53+
54+
function initShowMoreButton(container: HTMLElement): void {
55+
if (container.querySelector(".recentActivityList__showMoreButton")) {
56+
return;
57+
}
58+
59+
const div = document.createElement("div");
60+
div.classList.add("recentActivityList__showMoreButton");
61+
container.append(div);
62+
63+
const button = document.createElement("button");
64+
button.type = "button";
65+
button.classList.add("button", "small");
66+
button.textContent = getPhrase("wcf.like.reaction.more");
67+
div.append(button);
68+
69+
const small = document.createElement("small");
70+
small.textContent = getPhrase("wcf.like.reaction.noMoreEntries");
71+
small.hidden = true;
72+
div.append(small);
73+
74+
const hasItems = container.querySelectorAll(":scope > div").length > 2;
75+
if (!hasItems) {
76+
button.hidden = true;
77+
small.hidden = false;
78+
}
79+
80+
button.addEventListener(
81+
"click",
82+
promiseMutex(() => loadMore(container)),
83+
);
84+
}
85+
86+
function initTargetTypeButtons(container: HTMLElement): void {
87+
container.querySelectorAll<HTMLElement>("button[data-target-type]").forEach((button) => {
88+
button.addEventListener(
89+
"click",
90+
promiseMutex(() => {
91+
const targetType = button.dataset.targetType!;
92+
if (targetType === container.dataset.targetType) {
93+
return Promise.resolve();
94+
}
95+
96+
container.querySelector("button[data-target-type].active")!.classList.remove("active");
97+
button.classList.add("active");
98+
container.dataset.targetType = targetType;
99+
100+
return reload(container);
101+
}),
102+
);
103+
});
104+
}
105+
106+
function initReactionTypeButtons(container: HTMLElement): void {
107+
container.querySelectorAll<HTMLElement>("button[data-reaction-type-id]").forEach((button) => {
108+
button.addEventListener(
109+
"click",
110+
promiseMutex(() => {
111+
const reactionTypeID = button.dataset.reactionTypeId!;
112+
const activeButton = container.querySelector("button[data-reaction-type-id].active");
113+
114+
if (activeButton) {
115+
activeButton.classList.remove("active");
116+
}
117+
118+
if (container.dataset.reactionTypeId !== reactionTypeID) {
119+
button.classList.add("active");
120+
container.dataset.reactionTypeId = reactionTypeID;
121+
} else {
122+
container.dataset.reactionTypeId = "0";
123+
}
124+
125+
return reload(container);
126+
}),
127+
);
128+
});
129+
}
130+
131+
export function setup(container: HTMLElement): void {
132+
initShowMoreButton(container);
133+
initTargetTypeButtons(container);
134+
initReactionTypeButtons(container);
135+
}

0 commit comments

Comments
 (0)