List own comments #7171

Merged
infinite-persistence merged 7 commits from ip/own.comments into master 2021-10-01 14:10:27 +02:00
17 changed files with 472 additions and 39 deletions

View file

@ -111,14 +111,14 @@ declare type ReactionListResponse = {
declare type CommentListParams = {
page: number, // pagination: which page of results
page_size: number, // pagination: nr of comments to show in a page (max 200)
claim_id: string, // claim id of claim being commented on
claim_id?: string, // claim id of claim being commented on
channel_name?: string, // signing channel name of claim (enables 'commentsEnabled' check)
channel_id?: string, // signing channel claim id of claim (enables 'commentsEnabled' check)
author_claim_id?: string, // filters comments to just this author
parent_id?: string, // filters comments to those under this thread
top_level?: boolean, // filters to only top level comments
hidden?: boolean, // if true, will show hidden comments as well
sort_by?: number, // NEWEST=0, OLDEST=1, CONTROVERSY=2, POPULARITY=3,
sort_by?: number, // @see: ui/constants/comments.js::SORT_BY
};
declare type CommentListResponse = {

View file

@ -209,6 +209,9 @@
"Failed to copy.": "Failed to copy.",
"The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.": "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.",
"Connecting...": "Connecting...",
"Your comments": "Your comments",
"View your past comments.": "View your past comments.",
"Content or channel was deleted.": "Content or channel was deleted.",
"Comments": "Comments",
"Comment": "Comment",
"Comment --[button to submit something]--": "Comment",
@ -1460,6 +1463,7 @@
"Staked LBRY Credits": "Staked LBRY Credits",
"1 comment": "1 comment",
"%total_comments% comments": "%total_comments% comments",
"No comments": "No comments",
"Upvote": "Upvote",
"Downvote": "Downvote",
"You loved this": "You loved this",

View file

@ -70,6 +70,7 @@ type Props = {
streamingUrl: ?string,
getFile: (string) => void,
customShouldHide?: (Claim) => boolean,
searchParams?: { [string]: string },
showUnresolvedClaim?: boolean,
showNullPlaceholder?: boolean,
includeSupportAction?: boolean,
@ -125,6 +126,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
// modifiers
active,
customShouldHide,
searchParams,
showNullPlaceholder,
// value from show mature content user setting
// true if the user doesn't wanna see nsfw content
@ -221,6 +223,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
if (listId) {
navigateSearch.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
}
if (searchParams) {
Object.keys(searchParams).forEach((key) => {
navigateSearch.set(key, searchParams[key]);
});
}
const handleNavLinkClick = (e) => {
if (onClick) {

View file

@ -52,6 +52,7 @@ type Props = {
doToast: ({ message: string }) => void,
isTopLevel?: boolean,
threadDepth: number,
hideActions?: boolean,
isPinned: boolean,
othersReacts: ?{
like: number,
@ -95,6 +96,7 @@ function Comment(props: Props) {
doToast,
isTopLevel,
threadDepth,
hideActions,
isPinned,
othersReacts,
playingUri,
@ -348,18 +350,20 @@ function Comment(props: Props) {
)}
</div>
<div className="comment__actions">
{threadDepth !== 0 && (
<Button
requiresAuth={IS_WEB}
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
className="comment__action"
onClick={handleCommentReply}
icon={ICONS.REPLY}
/>
)}
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
</div>
{!hideActions && (
<div className="comment__actions">
{threadDepth !== 0 && (
<Button
requiresAuth={IS_WEB}
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
className="comment__action"
onClick={handleCommentReply}
icon={ICONS.REPLY}
/>
)}
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
</div>
)}
{numDirectReplies > 0 && !showReplies && (
<div className="comment__actions">

View file

@ -48,7 +48,7 @@ const perform = (dispatch) => ({
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
resetComments: (uri) => dispatch(doCommentReset(uri)),
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
});
export default connect(select, perform)(CommentsList);

View file

@ -151,10 +151,13 @@ function CommentList(props: Props) {
// Reset comments
useEffect(() => {
if (page === 0) {
resetComments(uri);
if (claim) {
resetComments(claim.claim_id);
}
setPage(1);
}
}, [page, uri, resetComments]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, uri, resetComments]); // 'claim' is derived from 'uri'
// Fetch top-level comments
useEffect(() => {

View file

@ -59,6 +59,7 @@ const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChu
const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "secondary" */));
const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "secondary" */));
const LivestreamCurrentPage = lazyImport(() => import('page/livestreamCurrent' /* webpackChunkName: "secondary" */));
const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */));
const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "secondary" */));
const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "secondary" */));
const PublishPage = lazyImport(() => import('page/publish' /* webpackChunkName: "secondary" */));
@ -329,6 +330,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.SWAP}`} component={SwapPage} />
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`} component={OwnComments} />
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux';
import { doWalletStatus, selectMyChannelClaims, selectWalletIsEncrypted } from 'lbry-redux';
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectLanguage } from 'redux/selectors/settings';
@ -9,6 +9,7 @@ const select = (state) => ({
isAuthenticated: selectUserVerifiedEmail(state),
walletEncrypted: selectWalletIsEncrypted(state),
user: selectUser(state),
myChannels: selectMyChannelClaims(state),
language: selectLanguage(state),
});

View file

@ -15,12 +15,13 @@ type Props = {
isAuthenticated: boolean,
walletEncrypted: boolean,
user: User,
myChannels: ?Array<ChannelClaim>,
// --- perform ---
doWalletStatus: () => void,
};
export default function SettingAccount(props: Props) {
const { isAuthenticated, walletEncrypted, user, doWalletStatus } = props;
const { isAuthenticated, walletEncrypted, user, myChannels, doWalletStatus } = props;
const [storedPassword, setStoredPassword] = React.useState(false);
// Determine if password is stored.
@ -92,6 +93,17 @@ export default function SettingAccount(props: Props) {
</SettingsRow>
)}
{/* @endif */}
{myChannels && (
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
<Button
button="inverse"
label={__('Manage')}
icon={ICONS.ARROW_RIGHT}
navigate={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`}
/>
</SettingsRow>
)}
</>
}
/>

View file

@ -9,6 +9,7 @@ export const SORT_BY = {
OLDEST: 1,
CONTROVERSY: 2,
POPULARITY: 3,
NEWEST_NO_PINS: 4,
};
export const BLOCK_LEVEL = {

View file

@ -52,6 +52,7 @@ export const PAGE_TITLE = {
[PAGES.SETTINGS_STRIPE_ACCOUNT]: 'Bank Accounts',
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
[PAGES.SWAP]: 'Swap Credits',
[PAGES.TAGS_FOLLOWING]: 'Tags',
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',

View file

@ -46,6 +46,7 @@ exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SETTINGS_CREATOR = 'settings/creator';
exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
exports.SETTINGS_OWN_COMMENTS = 'settings/ownComments';
exports.SHOW = 'show';
exports.ACCOUNT = 'account';
exports.SEARCH = 'search';

View file

@ -0,0 +1,33 @@
import { connect } from 'react-redux';
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import {
selectIsFetchingComments,
makeSelectCommentsForUri,
makeSelectTotalCommentsCountForUri,
makeSelectTopLevelTotalPagesForUri,
} from 'redux/selectors/comments';
import { selectClaimsById } from 'lbry-redux';
import OwnComments from './view';
const select = (state) => {
const activeChannelClaim = selectActiveChannelClaim(state);
const uri = activeChannelClaim && activeChannelClaim.canonical_url;
return {
activeChannelClaim,
allComments: makeSelectCommentsForUri(uri)(state),
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
isFetchingComments: selectIsFetchingComments(state),
claimsById: selectClaimsById(state),
};
};
const perform = (dispatch) => ({
doCommentReset: (a) => dispatch(doCommentReset(a)),
doCommentListOwn: (a, b, c) => dispatch(doCommentListOwn(a, b, c)),
});
export default connect(select, perform)(OwnComments);

View file

@ -0,0 +1,210 @@
// @flow
import React from 'react';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import ClaimPreview from 'component/claimPreview';
import Comment from 'component/comment';
import Card from 'component/common/card';
import Empty from 'component/common/empty';
import Page from 'component/page';
import Spinner from 'component/spinner';
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
import * as ICONS from 'constants/icons';
import useFetched from 'effects/use-fetched';
import debounce from 'util/debounce';
function scaleToDevicePixelRatio(value) {
const devicePixelRatio = window.devicePixelRatio || 1.0;
if (devicePixelRatio < 1.0) {
return Math.ceil(value / devicePixelRatio);
}
return Math.ceil(value * devicePixelRatio);
}
type Props = {
activeChannelClaim: ?ChannelClaim,
allComments: Array<Comment>,
totalComments: number,
topLevelTotalPages: number,
isFetchingComments: boolean,
claimsById: any,
doCommentReset: (claimId: string) => void,
doCommentListOwn: (channelId: string, page: number, pageSize: number) => void,
};
export default function OwnComments(props: Props) {
const {
activeChannelClaim,
allComments,
totalComments,
isFetchingComments,
claimsById,
doCommentReset,
doCommentListOwn,
} = props;
const spinnerRef = React.useRef();
const [page, setPage] = React.useState(0);
const [activeChannelId, setActiveChannelId] = React.useState('');
// Since we are sharing the key for Discussion and MyComments, don't show
// the list until we've gone through the initial reset.
const wasResetAndReady = useFetched(isFetchingComments);
const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL);
const moreBelow = page < totalPages;
function getCommentsElem(comments) {
return comments.map((comment) => {
const contentClaim = claimsById[comment.claim_id];
const isChannel = contentClaim && contentClaim.value_type === 'channel';
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
return (
<div key={comment.comment_id} className="comments-own card__main-actions">
<div className="section__actions">
<div className="comments-own--claim">
{contentClaim && (
<ClaimPreview
uri={contentClaim.canonical_url}
searchParams={{
...(isChannel ? { view: 'discussion' } : {}),
...(isLivestream ? {} : { lc: comment.comment_id }),
}}
hideActions
hideMenu
properties={() => null}
/>
)}
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
</div>
<Comment
isTopLevel
hideActions
authorUri={comment.channel_url}
author={comment.channel_name}
commentId={comment.comment_id}
message={comment.comment}
timePosted={comment.timestamp * 1000}
commentIsMine
supportAmount={comment.support_amount}
numDirectReplies={0} // Don't show replies here
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
/>
</div>
</div>
);
});
}
// Active channel changed
React.useEffect(() => {
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
setActiveChannelId(activeChannelClaim.claim_id);
setPage(0);
}
}, [activeChannelClaim, activeChannelId]);
// Reset comments
React.useEffect(() => {
if (page === 0 && activeChannelId) {
doCommentReset(activeChannelId);
setPage(1);
}
}, [page, activeChannelId]); // eslint-disable-line react-hooks/exhaustive-deps
// Fetch own comments
React.useEffect(() => {
if (page !== 0 && activeChannelId) {
doCommentListOwn(activeChannelId, page, COMMENT_PAGE_SIZE_TOP_LEVEL);
}
}, [page]); // eslint-disable-line react-hooks/exhaustive-deps
// Infinite scroll
React.useEffect(() => {
function shouldFetchNextPage(page, topLevelTotalPages, window, document, yPrefetchPx = 1000) {
if (!spinnerRef || !spinnerRef.current) {
return false;
}
const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe
const windowH = window.innerHeight || document.documentElement.clientHeight; // $FlowFixMe
const windowW = window.innerWidth || document.documentElement.clientWidth; // $FlowFixMe
const isApproachingViewport = yPrefetchPx !== 0 && rect.top < windowH + scaleToDevicePixelRatio(yPrefetchPx);
const isInViewport =
rect.width > 0 &&
rect.height > 0 &&
rect.bottom >= 0 &&
rect.right >= 0 &&
// $FlowFixMe
rect.top <= windowH &&
// $FlowFixMe
rect.left <= windowW;
return (isInViewport || isApproachingViewport) && page < topLevelTotalPages;
}
const handleCommentScroll = debounce(() => {
if (shouldFetchNextPage(page, totalPages, window, document)) {
setPage(page + 1);
}
}, 200);
if (!isFetchingComments && moreBelow && spinnerRef && spinnerRef.current) {
if (shouldFetchNextPage(page, totalPages, window, document, 0)) {
setPage(page + 1);
} else {
window.addEventListener('scroll', handleCommentScroll);
return () => window.removeEventListener('scroll', handleCommentScroll);
}
}
}, [page, spinnerRef, isFetchingComments, moreBelow, totalPages]);
// **************************************************************************
// **************************************************************************
if (!activeChannelClaim) {
return null;
}
return (
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}>
<ChannelSelector hideAnon />
<Card
isBodyList
title={
totalComments > 0
? totalComments === 1
? __('1 comment')
: __('%total_comments% comments', { total_comments: totalComments })
: isFetchingComments
? ''
: __('No comments')
}
titleActions={
<Button
button="alt"
icon={ICONS.REFRESH}
title={__('Refresh')}
onClick={() => {
setPage(0);
}}
/>
}
body={
<>
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}
{(isFetchingComments || moreBelow) && (
<div className="main--empty" ref={spinnerRef}>
<Spinner type="small" />
</div>
)}
</>
}
/>
</Page>
);
}

View file

@ -3,7 +3,15 @@ import * as ACTIONS from 'constants/action_types';
import * as REACTION_TYPES from 'constants/reactions';
import * as PAGES from 'constants/pages';
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
import { Lbry, parseURI, buildURI, selectClaimsByUri, selectMyChannelClaims, isURIEqual } from 'lbry-redux';
import {
Lbry,
parseURI,
buildURI,
selectClaimsByUri,
selectMyChannelClaims,
isURIEqual,
doClaimSearch,
} from 'lbry-redux';
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
import {
makeSelectMyReactionsForComment,
@ -127,7 +135,6 @@ export function doCommentList(
type: ACTIONS.COMMENT_LIST_FAILED,
data: 'unable to find claim for uri',
});
return;
}
@ -139,7 +146,7 @@ export function doCommentList(
});
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
const authorChannelClaim = claim.value_type === 'channel' ? claim : claim.signing_channel;
const creatorChannelClaim = claim.value_type === 'channel' ? claim : claim.signing_channel;
return Comments.comment_list({
page,
@ -147,8 +154,8 @@ export function doCommentList(
page_size: pageSize,
parent_id: parentId || undefined,
top_level: !parentId,
channel_id: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
channel_name: authorChannelClaim ? authorChannelClaim.name : undefined,
channel_id: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
channel_name: creatorChannelClaim ? creatorChannelClaim.name : undefined,
sort_by: sortBy,
})
.then((result: CommentListResponse) => {
@ -162,7 +169,7 @@ export function doCommentList(
totalFilteredItems: total_filtered_items,
totalPages: total_pages,
claimId: claimId,
commenterClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
creatorClaimId: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
uri: uri,
},
});
@ -175,7 +182,7 @@ export function doCommentList(
dispatch({
type: ACTIONS.COMMENT_LIST_COMPLETED,
data: {
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
creatorClaimId: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
disabled: true,
},
});
@ -201,6 +208,103 @@ export function doCommentList(
};
}
export function doCommentListOwn(
channelId: string,
page: number = 1,
pageSize: number = 99999,
sortBy: number = SORT_BY.NEWEST_NO_PINS
) {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const myChannelClaims = selectMyChannelClaims(state);
if (!myChannelClaims) {
console.error('Failed to fetch channel list.'); // eslint-disable-line
return;
}
const channelClaim = myChannelClaims.find((x) => x.claim_id === channelId);
if (!channelClaim) {
console.error('You do not own this channel.'); // eslint-disable-line
return;
}
const channelSignature = await channelSignName(channelClaim.claim_id, channelClaim.name);
if (!channelSignature) {
console.error('Failed to sign channel name.'); // eslint-disable-line
return;
}
dispatch({
type: ACTIONS.COMMENT_LIST_STARTED,
data: {},
});
return Comments.comment_list({
page,
page_size: pageSize,
sort_by: sortBy,
author_claim_id: channelId,
requestor_channel_name: channelClaim.name,
requestor_channel_id: channelClaim.claim_id,
signature: channelSignature.signature,
signing_ts: channelSignature.signing_ts,
})
.then((result: CommentListResponse) => {
const { items: comments, total_items, total_filtered_items, total_pages } = result;
if (!comments) {
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: 'No more comments.' });
return;
}
dispatch(
doClaimSearch({
page: 1,
page_size: 20,
no_totals: true,
claim_ids: comments.map((c) => c.claim_id),
})
)
.then((result) => {
dispatch({
type: ACTIONS.COMMENT_LIST_COMPLETED,
data: {
comments,
totalItems: total_items,
totalFilteredItems: total_filtered_items,
totalPages: total_pages,
uri: channelClaim.canonical_url, // hijack "Discussion Page"
claimId: channelClaim.claim_id, // hijack "Discussion Page"
},
});
})
.catch((err) => {
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: err });
});
})
.catch((error) => {
switch (error.message) {
case FETCH_API_FAILED_TO_FETCH:
dispatch(
doToast({
isError: true,
message: Comments.isCustomServer
? __('Failed to fetch comments. Verify custom server settings.')
: __('Failed to fetch comments.'),
})
);
dispatch(doToast({ isError: true, message: `${error.message}` }));
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
break;
default:
dispatch(doToast({ isError: true, message: `${error.message}` }));
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
}
});
};
}
export function doCommentById(commentId: string, toastIfNotFound: boolean = true) {
return (dispatch: Dispatch, getState: GetState) => {
dispatch({
@ -239,17 +343,10 @@ export function doCommentById(commentId: string, toastIfNotFound: boolean = true
};
}
export function doCommentReset(uri: string) {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const claim = selectClaimsByUri(state)[uri];
const claimId = claim ? claim.claim_id : null;
export function doCommentReset(claimId: string) {
return (dispatch: Dispatch) => {
if (!claimId) {
dispatch({
type: ACTIONS.COMMENT_LIST_FAILED,
data: 'unable to find claim for uri',
});
console.error(`Failed to reset comments`); //eslint-disable-line
return;
}

View file

@ -246,7 +246,7 @@ export default handleActions(
claimId,
uri,
disabled,
commenterClaimId,
creatorClaimId,
} = action.data;
const commentById = Object.assign({}, state.commentById);
@ -262,8 +262,8 @@ export default handleActions(
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
settingsByChannelId[commenterClaimId] = {
...(settingsByChannelId[commenterClaimId] || {}),
settingsByChannelId[creatorClaimId] = {
...(settingsByChannelId[creatorClaimId] || {}),
comments_enabled: !disabled,
};

View file

@ -487,3 +487,60 @@ $thumbnailWidthSmall: 1rem;
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
}
}
.comments-own {
.section__actions {
align-items: flex-start;
}
.comments-own--claim {
min-width: 100%;
max-width: 100%;
@media (min-width: $breakpoint-medium) {
min-width: 40%;
max-width: 40%;
}
.media__thumb {
flex-shrink: 0;
overflow: hidden;
$width: 5rem;
@include handleClaimListGifThumbnail($width);
width: $width;
height: calc(#{$width} * (9 / 16));
margin-right: var(--spacing-s);
}
.channel-thumbnail {
@include handleChannelGif(calc(5rem * 9 / 16));
margin-right: var(--spacing-xs);
@media (min-width: $breakpoint-small) {
@include handleChannelGif(calc(5rem * 9 / 16));
margin-right: var(--spacing-s);
}
}
}
.claim-preview__wrapper {
margin: 0 0;
padding: 0;
@media (min-width: $breakpoint-medium) {
margin: 0 var(--spacing-xs);
}
}
.comment {
margin-top: var(--spacing-s);
margin-left: 0;
padding-left: var(--spacing-m);
border-left: 4px solid var(--color-border);
@media (min-width: $breakpoint-medium) {
margin-top: 0;
margin-left: var(--spacing-s);
}
}
}