Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions com.woltlab.wcf/templates/userProfileLikeItem.tpl
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
{foreach from=$likeList item=like}
<li>
<div class="box48">
{foreach from=$likeList item='like'}
<div class="
recentActivityListItem
recentActivityListItem--withDescription
{if $like->isIgnoredContent()}ignoredUserContent{/if}
">
<div class="recentActivityListItem__avatar">
{user object=$like->getUserProfile() type='avatar48' ariaHidden='true' tabindex='-1'}

<div>
<div class="containerHeadline">
<h3>
{user object=$like->getUserProfile()}
<small class="separatorLeft">{time time=$like->time}</small>
</h3>
<div>{unsafe:$like->getTitle()}</div>
<small class="containerContentType">{$like->getObjectTypeDescription()}</small>
</div>

<div class="containerContent">{unsafe:$like->getDescription()}</div>
</div>

<h3 class="recentActivityListItem__title">
{if $like->getLink()}
<a href="{$like->getLink()}" class="recentActivityListItem__link">{unsafe:$like->getTitle()}</a>
{else}
{unsafe:$like->getTitle()}
{/if}
</h3>

{if $like->getDescription()}
<div class="recentActivityListItem__description">
{unsafe:$like->getDescription()}
</div>
{/if}

<div class="recentActivityListItem__time">
{time time=$like->time}
</div>
</li>
</div>
{/foreach}
65 changes: 43 additions & 22 deletions com.woltlab.wcf/templates/userProfileLikes.tpl
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
<script data-relocate="true">
require(['WoltLabSuite/Core/Ui/Reaction/Profile/Loader', 'Language'], function(UiReactionProfileLoader, Language) {
Language.addObject({
'wcf.like.reaction.noMoreEntries': '{jslang}wcf.like.reaction.noMoreEntries{/jslang}',
'wcf.like.reaction.more': '{jslang}wcf.like.reaction.more{/jslang}'
});
new UiReactionProfileLoader({$userID});
});
</script>

<ul id="likeList" class="containerList recentActivityList likeList" data-last-like-time="{$lastLikeTime}">
<li class="containerListButtonGroup likeTypeSelection">
<ul class="buttonGroup" id="likeType">
<li><a class="button small active" data-like-type="received">{lang}wcf.like.reactionsReceived{/lang}</a></li>
<li><a class="button small" data-like-type="given">{lang}wcf.like.reactionsGiven{/lang}</a></li>
<div id="reactionList" class="recentActivityList recentActivityList--userProfileContent userProfileReactionList"
data-last-like-time="{$lastLikeTime}"
data-user-id="{$userID}"
data-target-type="received"
data-reaction-type-id="0"
>
<div class="userProfileReactionList__typeSelection">
<ul class="buttonGroup">
<li>
<button type="button" class="button small active" data-target-type="received">
{lang}wcf.like.reactionsReceived{/lang}
</button>
</li>
<li>
<button type="button" class="button small" data-target-type="given">
{lang}wcf.like.reactionsGiven{/lang}
</button>
</li>
</ul>

{if $__wcf->getReactionHandler()->getReactionTypes()|count > 1}
<ul class="buttonGroup" id="reactionType">
<ul class="buttonGroup">
{foreach from=$__wcf->getReactionHandler()->getReactionTypes() item=reactionType name=reactionTypeLoop}
<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>
<li>
<button
type="button"
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()}
</button>
</li>
{/foreach}
</ul>
{/if}
</li>
</div>

{include file='userProfileLikeItem'}
</ul>
</div>

<script data-relocate="true">
require(['WoltLabSuite/Core/Component/User/Reaction/Loader'], ({ setup }) => {
{jsphrase name='wcf.like.reaction.noMoreEntries'}
{jsphrase name='wcf.like.reaction.more'}
setup(document.getElementById('reactionList'));
});
</script>
37 changes: 37 additions & 0 deletions ts/WoltLabSuite/Core/Api/Users/Reactions/RenderUserReactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Loads a paginated list of reactions for a user profile.
*
* @author Marcel Werk
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/

import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "../../Result";

type Response = {
lastLikeTime: number;
template: string;
};

export async function renderUserReactions(
userID: number,
targetType: string,
lastLikeTime: number = 0,
reactionTypeID: number = 0,
): Promise<ApiResult<Response | Record<string, never>>> {
const url = new URL(`${window.WSC_RPC_API_URL}core/users/${userID}/reactions/render`);
url.searchParams.set("targetType", targetType);
url.searchParams.set("lastLikeTime", lastLikeTime.toString());
url.searchParams.set("reactionTypeID", reactionTypeID.toString());

let response: Response | Record<string, never>;
try {
response = (await prepareRequest(url).get().fetchAsJson()) as Response | Record<string, never>;
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue(response);
}
135 changes: 135 additions & 0 deletions ts/WoltLabSuite/Core/Component/User/Reaction/Loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Handles the reaction list in the user profile.
*
* @author Marcel Werk
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/

import DomUtil from "WoltLabSuite/Core/Dom/Util";
import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
import { getPhrase } from "WoltLabSuite/Core/Language";
import { renderUserReactions } from "WoltLabSuite/Core/Api/Users/Reactions/RenderUserReactions";

async function loadMore(container: HTMLElement): Promise<void> {
const result = await renderUserReactions(
parseInt(container.dataset.userId || "0"),
container.dataset.targetType || "received",
parseInt(container.dataset.lastLikeTime || "0"),
parseInt(container.dataset.reactionTypeId || "0"),
);
const response = result.unwrap();

if ("template" in response) {
container.dataset.lastLikeTime = response.lastLikeTime.toString();

const showMoreButton = container.querySelector(".recentActivityList__showMoreButton")!;
const fragment = DomUtil.createFragmentFromHtml(response.template);
container.insertBefore(fragment, showMoreButton);

showMoreButton.querySelector("button")!.hidden = false;
showMoreButton.querySelector("small")!.hidden = true;
} else {
const showMoreButton = container.querySelector(".recentActivityList__showMoreButton")!;
showMoreButton.querySelector("button")!.hidden = true;
showMoreButton.querySelector("small")!.hidden = false;
}
}

async function reload(container: HTMLElement): Promise<void> {
container.querySelectorAll(":scope > div:not(:first-child):not(:last-child)").forEach((el) => el.remove());

container.dataset.lastLikeTime = "0";

const showMoreButton = container.querySelector(".recentActivityList__showMoreButton");
if (showMoreButton !== null) {
showMoreButton.querySelector("button")!.hidden = false;
showMoreButton.querySelector("small")!.hidden = true;
}

await loadMore(container);
}

function initShowMoreButton(container: HTMLElement): void {
if (container.querySelector(".recentActivityList__showMoreButton")) {
return;
}

const div = document.createElement("div");
div.classList.add("recentActivityList__showMoreButton");
container.append(div);

const button = document.createElement("button");
button.type = "button";
button.classList.add("button", "small");
button.textContent = getPhrase("wcf.like.reaction.more");
div.append(button);

const small = document.createElement("small");
small.textContent = getPhrase("wcf.like.reaction.noMoreEntries");
small.hidden = true;
div.append(small);

const hasItems = container.querySelectorAll(":scope > div").length > 2;
if (!hasItems) {
button.hidden = true;
small.hidden = false;
}

button.addEventListener(
"click",
promiseMutex(() => loadMore(container)),
);
}

function initTargetTypeButtons(container: HTMLElement): void {
container.querySelectorAll<HTMLElement>("button[data-target-type]").forEach((button) => {
button.addEventListener(
"click",
promiseMutex(() => {
const targetType = button.dataset.targetType!;
if (targetType === container.dataset.targetType) {
return Promise.resolve();
}

container.querySelector("button[data-target-type].active")!.classList.remove("active");
button.classList.add("active");
container.dataset.targetType = targetType;

return reload(container);
}),
);
});
}

function initReactionTypeButtons(container: HTMLElement): void {
container.querySelectorAll<HTMLElement>("button[data-reaction-type-id]").forEach((button) => {
button.addEventListener(
"click",
promiseMutex(() => {
const reactionTypeID = button.dataset.reactionTypeId!;
const activeButton = container.querySelector("button[data-reaction-type-id].active");

if (activeButton) {
activeButton.classList.remove("active");
}

if (container.dataset.reactionTypeId !== reactionTypeID) {
button.classList.add("active");
container.dataset.reactionTypeId = reactionTypeID;
} else {
container.dataset.reactionTypeId = "0";
}

return reload(container);
}),
);
});
}

export function setup(container: HTMLElement): void {
initShowMoreButton(container);
initTargetTypeButtons(container);
initReactionTypeButtons(container);
}
Loading