postmerge: loadmorewrappers

This commit is contained in:
Untone 2024-07-21 17:25:46 +03:00
parent 87d08dcb75
commit 0061b68257
8 changed files with 152 additions and 152 deletions

View File

@ -5,10 +5,18 @@ import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/core.gen' import {
Author,
QueryLoad_Reactions_ByArgs,
Reaction,
ReactionKind,
ReactionSort
} from '~/graphql/schema/core.gen'
import { byCreated, byStat } from '~/lib/sort' import { byCreated, byStat } from '~/lib/sort'
import { SortFunction } from '~/types/common' import { SortFunction } from '~/types/common'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { InlineLoader } from '../_shared/InlineLoader'
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import styles from './Article.module.scss' import styles from './Article.module.scss'
import { Comment } from './Comment' import { Comment } from './Comment'
@ -20,6 +28,7 @@ type Props = {
shoutSlug: string shoutSlug: string
shoutId: number shoutId: number
} }
const COMMENTS_PER_PAGE = 50
export const CommentsTree = (props: Props) => { export const CommentsTree = (props: Props) => {
const { session } = useSession() const { session } = useSession()
@ -29,7 +38,7 @@ export const CommentsTree = (props: Props) => {
const [newReactions, setNewReactions] = createSignal<Reaction[]>([]) const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>() const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const { reactionEntities, createReaction, loadReactionsBy } = useReactions() const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions()
const comments = createMemo(() => const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
@ -89,6 +98,23 @@ export const CommentsTree = (props: Props) => {
setClearEditor(false) setClearEditor(false)
setPosting(false) setPosting(false)
} }
const [commentsLoading, setCommentsLoading] = createSignal(false)
const [pagination, setPagination] = createSignal(0)
const loadMoreComments = async () => {
setCommentsLoading(true)
const next = pagination() + 1
const offset = next * COMMENTS_PER_PAGE
const opts: QueryLoad_Reactions_ByArgs = {
by: { comment: true, shout: props.shoutSlug },
limit: COMMENTS_PER_PAGE,
offset
}
const rrr = await loadReactionsBy(opts)
rrr && addReactions(rrr)
rrr && setPagination(next)
setCommentsLoading(false)
return rrr as LoadMoreItems
}
return ( return (
<> <>
@ -127,20 +153,31 @@ export const CommentsTree = (props: Props) => {
</ul> </ul>
</Show> </Show>
</div> </div>
<ul class={styles.comments}> <Show when={commentsLoading()}>
<For each={sortedComments().filter((r) => !r.reply_to)}> <InlineLoader />
{(reaction) => ( </Show>
<Comment <LoadMoreWrapper
sortedComments={sortedComments()} loadFunction={loadMoreComments}
isArticleAuthor={Boolean(props.articleAuthors.some((a) => a?.id === reaction.created_by.id))} pageSize={COMMENTS_PER_PAGE}
comment={reaction} hidden={commentsLoading()}
clickedReply={(id) => setClickedReplyId(id)} >
clickedReplyId={clickedReplyId()} <ul class={styles.comments}>
lastSeen={shoutLastSeen()} <For each={sortedComments().filter((r) => !r.reply_to)}>
/> {(reaction) => (
)} <Comment
</For> sortedComments={sortedComments()}
</ul> isArticleAuthor={Boolean(
props.articleAuthors.some((a) => a?.id === reaction.created_by.id)
)}
comment={reaction}
clickedReply={(id) => setClickedReplyId(id)}
clickedReplyId={clickedReplyId()}
lastSeen={shoutLastSeen()}
/>
)}
</For>
</ul>
</LoadMoreWrapper>
<ShowIfAuthenticated <ShowIfAuthenticated
fallback={ fallback={
<div class={styles.signInMessage}> <div class={styles.signInMessage}>

View File

@ -6,10 +6,9 @@ import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMou
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { useFeed } from '~/context/feed' import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui' import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen' import { type Author, type Maybe, type Shout, type Topic } from '~/graphql/schema/core.gen'
import { processPrepositions } from '~/intl/prepositions' import { processPrepositions } from '~/intl/prepositions'
import { isCyrillic } from '~/intl/translate' import { isCyrillic } from '~/intl/translate'
import { getImageUrl } from '~/lib/getThumbUrl' import { getImageUrl } from '~/lib/getThumbUrl'
@ -63,15 +62,11 @@ const scrollTo = (el: HTMLElement) => {
} }
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
const COMMENTS_PER_PAGE = 30
const VOTES_PER_PAGE = 50
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>() const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>()
const { showModal } = useUI() const { showModal } = useUI()
const { loadReactionsBy } = useReactions()
const [selectedImage, setSelectedImage] = createSignal('') const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const { session, requireAuthentication } = useSession() const { session, requireAuthentication } = useSession()
@ -79,27 +74,6 @@ export const FullArticle = (props: Props) => {
const { addSeen } = useFeed() const { addSeen } = useFeed()
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000))) const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
const [pages, setPages] = createSignal<Record<string, number>>({})
createEffect(
on(
pages,
async (p: Record<string, number>) => {
await loadReactionsBy({
by: { shout: props.article.slug, comment: true },
limit: COMMENTS_PER_PAGE,
offset: COMMENTS_PER_PAGE * p.comments || 0
})
await loadReactionsBy({
by: { shout: props.article.slug, rating: true },
limit: VOTES_PER_PAGE,
offset: VOTES_PER_PAGE * p.rating || 0
})
setIsReactionsLoaded(true)
},
{ defer: true }
)
)
const canEdit = createMemo( const canEdit = createMemo(
() => () =>
Boolean(author()?.id) && Boolean(author()?.id) &&
@ -167,7 +141,7 @@ export const FullArticle = (props: Props) => {
let commentsRef: HTMLDivElement | undefined let commentsRef: HTMLDivElement | undefined
createEffect(() => { createEffect(() => {
if (searchParams?.commentId && isReactionsLoaded()) { if (searchParams?.commentId) {
const commentElement = document.querySelector<HTMLElement>( const commentElement = document.querySelector<HTMLElement>(
`[id='comment_${searchParams?.commentId}']` `[id='comment_${searchParams?.commentId}']`
) )
@ -314,11 +288,8 @@ export const FullArticle = (props: Props) => {
} }
) )
) )
const [ratings, setRatings] = createSignal<Reaction[]>([])
onMount(async () => { onMount(async () => {
// install('G-LQ4B87H8C2')
await loadReactionsBy({ by: { shout: props.article.slug } })
addSeen(props.article.slug) addSeen(props.article.slug)
document.title = props.article.title document.title = props.article.title
updateIframeSizes() updateIframeSizes()
@ -453,11 +424,7 @@ export const FullArticle = (props: Props) => {
<div class="col-md-16 offset-md-5"> <div class="col-md-16 offset-md-5">
<div class={styles.shoutStats}> <div class={styles.shoutStats}>
<div class={styles.shoutStatsItem}> <div class={styles.shoutStatsItem}>
<ShoutRatingControl <ShoutRatingControl shout={props.article} class={styles.ratingControl} />
shout={props.article}
class={styles.ratingControl}
ratings={ratings()}
/>
</div> </div>
<Popover content={t('Comment')} disabled={isActionPopupActive()}> <Popover content={t('Comment')} disabled={isActionPopupActive()}>
@ -593,13 +560,11 @@ export const FullArticle = (props: Props) => {
</For> </For>
</div> </div>
<div id="comments" ref={(el) => (commentsRef = el)}> <div id="comments" ref={(el) => (commentsRef = el)}>
<Show when={isReactionsLoaded()}> <CommentsTree
<CommentsTree shoutId={props.article.id}
shoutId={props.article.id} shoutSlug={props.article.slug}
shoutSlug={props.article.slug} articleAuthors={props.article.authors as Author[]}
articleAuthors={props.article.authors as Author[]} />
/>
</Show>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,15 +1,15 @@
import { useSearchParams } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createEffect, createMemo, createSignal, on } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { byCreated } from '~/lib/sort'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/ui'
import { Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen' import { QueryLoad_Reactions_ByArgs, Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
import { useRouter } from '../../stores/router'
import { loadShout } from '../../stores/zine/articles'
import { byCreated } from '../../utils/sortby'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { InlineLoader } from '../_shared/InlineLoader'
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList' import { VotersList } from '../_shared/VotersList'
import stylesComment from './CommentRatingControl.module.scss' import stylesComment from './CommentRatingControl.module.scss'
@ -18,38 +18,40 @@ import stylesShout from './ShoutRatingControl.module.scss'
interface RatingControlProps { interface RatingControlProps {
shout?: Shout shout?: Shout
comment?: Reaction comment?: Reaction
ratings?: Reaction[]
class?: string class?: string
} }
export const RatingControl = (props: RatingControlProps) => { export const RatingControl = (props: RatingControlProps) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { changeSearchParams } = useRouter() const [_, changeSearchParams] = useSearchParams()
const snackbar = useSnackbar() const snackbar = useSnackbar()
const { author } = useSession() const { session } = useSession()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() const { addReactions } = useReactions()
const { reactionEntities, reactionsByShout, createReaction, deleteReaction, loadReactionsBy } =
useReactions()
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
const [ratingReactions, setRatingReactions] = createSignal<Reaction[]>([])
const [isLoading, setIsLoading] = createSignal(false)
// reaction kind
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some( Object.values(reactionEntities).some(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.created_by.slug === author()?.slug && r.created_by.slug === session()?.user?.app_data?.profile?.slug &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment?.shout.id &&
r.reply_to === props.comment.id, r.reply_to === props.comment?.id
) )
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like)) const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike)) const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
const [total, setTotal] = createSignal(props.comment?.stat?.rating || props.shout?.stat?.rating || 0)
const [ratingReactions, setRatingReactions] = createSignal<Reaction[]>([])
createEffect(() => { createEffect(() => {
const shout = props.comment.shout.id || props.shout.id const shout = props.comment?.shout.id || props.shout?.id
if (shout && !ratingReactions()) { if (shout && !ratingReactions()) {
let result = Object.values(reactionEntities).filter( let result = Object.values(reactionEntities).filter(
(r) => [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && r.shout.id === shout, (r) => [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && r.shout.id === shout
) )
if (props.comment?.id) result = result.filter((r) => r.reply_to === props.comment.id) if (props.comment?.id) result = result.filter((r) => r.reply_to === props.comment?.id)
setRatingReactions(result) setRatingReactions(result)
} }
}) })
@ -58,14 +60,14 @@ export const RatingControl = (props: RatingControlProps) => {
const reactionToDelete = Object.values(reactionEntities).find( const reactionToDelete = Object.values(reactionEntities).find(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.created_by.slug === author()?.slug && r.created_by.slug === session()?.user?.nickname &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment?.shout.id &&
r.reply_to === props.comment.id, r.reply_to === props.comment?.id
) )
return deleteReaction(reactionToDelete.id) return reactionToDelete && deleteReaction(reactionToDelete.id)
} }
const [isLoading, setIsLoading] = createSignal(false) // rating change
const handleRatingChange = async (isUpvote: boolean) => { const handleRatingChange = async (isUpvote: boolean) => {
setIsLoading(true) setIsLoading(true)
try { try {
@ -74,63 +76,33 @@ export const RatingControl = (props: RatingControlProps) => {
} else if (isDownvoted()) { } else if (isDownvoted()) {
await deleteRating(ReactionKind.Dislike) await deleteRating(ReactionKind.Dislike)
} else { } else {
await createReaction({ props.comment?.shout.id &&
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, (await createReaction({
shout: props.comment.shout.id, reaction: {
reply_to: props.comment.id, kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
}) shout: props.comment.shout.id,
reply_to: props.comment?.id
}
}))
} }
} catch { } catch {
snackbar?.showSnackbar({ type: 'error', body: t('Error') }) snackbar?.showSnackbar({ type: 'error', body: t('Error') })
} }
await loadShout(props.comment.shout.slug) if (props.comment?.shout.slug) {
await loadReactionsBy({ const rrr = await loadReactionsBy({ by: { shout: props.comment.shout.slug } })
by: { shout: props.comment.shout.slug }, addReactions(rrr)
}) }
setIsLoading(false) setIsLoading(false)
} }
createEffect( const total = createMemo<number>(() =>
on( props.comment?.stat?.rating ? props.comment.stat.rating : props.shout?.stat?.rating || 0
() => props.comment,
(comment) => {
if (comment) {
setTotal(comment?.stat?.rating)
}
},
{ defer: true },
),
) )
createEffect( createEffect(
on( on(
() => props.shout, [ratingReactions, () => session()?.user?.app_data?.profile],
(shout) => {
if (shout) {
setTotal(shout.stat?.rating)
}
},
{ defer: true },
),
)
createEffect(
on(
() => reactionEntities,
(reactions) => {
const ratings = Object.values(reactions).filter((r) => !r?.reply_to)
const likes = ratings.filter((rating) => rating.kind === 'LIKE').length
const dislikes = ratings.filter((rating) => rating.kind === 'DISLIKE').length
const total = likes - dislikes
setTotal(total)
},
{ defer: true },
),
)
createEffect(
on(
[ratingReactions, author],
([reactions, me]) => { ([reactions, me]) => {
console.debug('[RatingControl] on reactions update') console.debug('[RatingControl] on reactions update')
const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to) const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to)
@ -138,8 +110,8 @@ export const RatingControl = (props: RatingControlProps) => {
const myReaction = reactions.find((r) => r.created_by.id === me?.id) const myReaction = reactions.find((r) => r.created_by.id === me?.id)
setMyRate((_) => myReaction) setMyRate((_) => myReaction)
}, },
{ defer: true }, { defer: true }
), )
) )
const getTrigger = createMemo(() => { const getTrigger = createMemo(() => {
@ -148,48 +120,78 @@ export const RatingControl = (props: RatingControlProps) => {
class={clsx(stylesComment.commentRatingValue, { class={clsx(stylesComment.commentRatingValue, {
[stylesComment.commentRatingPositive]: total() > 0 && Boolean(props.comment?.id), [stylesComment.commentRatingPositive]: total() > 0 && Boolean(props.comment?.id),
[stylesComment.commentRatingNegative]: total() < 0 && Boolean(props.comment?.id), [stylesComment.commentRatingNegative]: total() < 0 && Boolean(props.comment?.id),
[stylesShout.ratingValue]: !props.comment?.id, [stylesShout.ratingValue]: !props.comment?.id
})} })}
> >
{total()} {total()}
</div> </div>
) )
}) })
const VOTERS_PER_PAGE = 10
const [ratingPage, setRatingPage] = createSignal(0)
const [ratingLoading, setRatingLoading] = createSignal(false) // FIXME: use loading indication
const ratings = createMemo(() =>
props.shout
? reactionsByShout[props.shout?.slug]?.filter(
(r) => r.kind === ReactionKind.Like || r.kind === ReactionKind.Dislike
)
: []
)
const loadMoreReactions = async () => {
setRatingLoading(true)
const next = ratingPage() + 1
const offset = VOTERS_PER_PAGE * next
const opts: QueryLoad_Reactions_ByArgs = {
by: { rating: true, shout: props.shout?.slug },
limit: VOTERS_PER_PAGE,
offset
}
const rrr = await loadReactionsBy(opts)
rrr && addReactions(rrr)
rrr && setRatingPage(next)
setRatingLoading(false)
return rrr as LoadMoreItems
}
return props.comment?.id ? ( return props.comment?.id ? (
<div class={stylesComment.commentRating}> <div class={stylesComment.commentRating}>
<button <button
role="button" role="button"
disabled={!author()} disabled={!session()?.user?.app_data?.profile}
onClick={() => handleRatingChange(true)} onClick={() => handleRatingChange(true)}
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, { class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
[stylesComment.voted]: isUpvoted(), [stylesComment.voted]: isUpvoted()
})} })}
/> />
<Popup <Popup
trigger={ trigger={
<div <div
class={clsx(stylesComment.commentRatingValue, { class={clsx(stylesComment.commentRatingValue, {
[stylesComment.commentRatingPositive]: props.comment.stat.rating > 0, [stylesComment.commentRatingPositive]: (props.comment?.stat?.rating || 0) > 0,
[stylesComment.commentRatingNegative]: props.comment.stat.rating < 0, [stylesComment.commentRatingNegative]: (props.comment?.stat?.rating || 0) < 0
})} })}
> >
{props.comment.stat.rating || 0} {props.comment?.stat?.rating || 0}
</div> </div>
} }
variant="tiny" variant="tiny"
> >
<VotersList <Show when={ratingLoading()}>
reactions={ratingReactions()} <InlineLoader />
fallbackMessage={t('This comment has not yet been rated')} </Show>
/> <LoadMoreWrapper
loadFunction={loadMoreReactions}
pageSize={VOTERS_PER_PAGE}
hidden={ratingLoading()}
>
<VotersList reactions={ratings()} fallbackMessage={t('This comment has not been rated yet')} />
</LoadMoreWrapper>
</Popup> </Popup>
<button <button
role="button" role="button"
disabled={!author()} disabled={!session()?.user?.app_data?.profile}
onClick={() => handleRatingChange(false)} onClick={() => handleRatingChange(false)}
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, { class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
[stylesComment.voted]: isDownvoted(), [stylesComment.voted]: isDownvoted()
})} })}
/> />
</div> </div>
@ -201,7 +203,7 @@ export const RatingControl = (props: RatingControlProps) => {
class={ class={
props.comment props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, { ? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
[stylesComment.voted]: myRate()?.kind === 'LIKE', [stylesComment.voted]: myRate()?.kind === 'LIKE'
}) })
: '' : ''
} }
@ -215,10 +217,10 @@ export const RatingControl = (props: RatingControlProps) => {
</button> </button>
<Popup trigger={getTrigger()} variant="tiny"> <Popup trigger={getTrigger()} variant="tiny">
<Show <Show
when={author()} when={!!session()?.user?.app_data?.profile}
fallback={ fallback={
<> <>
<span class="link" onClick={() => changeSearchParams({ mode: 'login', modal: 'auth' })}> <span class="link" onClick={() => changeSearchParams({ mode: 'login', m: 'auth' })}>
{t('Enter')} {t('Enter')}
</span> </span>
{lang() === 'ru' ? ', ' : ' '} {lang() === 'ru' ? ', ' : ' '}
@ -238,7 +240,7 @@ export const RatingControl = (props: RatingControlProps) => {
class={ class={
props.comment props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, { ? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
[stylesComment.voted]: myRate()?.kind === 'DISLIKE', [stylesComment.voted]: myRate()?.kind === 'DISLIKE'
}) })
: '' : ''
} }

View File

@ -1 +0,0 @@

View File

@ -456,7 +456,7 @@
"Theory": "Теории", "Theory": "Теории",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?", "There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?", "There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил", "This comment has not been rated yet": "Этот комментарий еще пока никто не оценил",
"This content is not published yet": "Содержимое ещё не опубликовано", "This content is not published yet": "Содержимое ещё не опубликовано",
"This email is": "Этот email", "This email is": "Этот email",
"This email is not verified": "Этот email не подтвержден", "This email is not verified": "Этот email не подтвержден",

View File

@ -1 +0,0 @@