Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a804d094f |
@@ -10,9 +10,7 @@
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
deriveUserWotScore,
|
||||
followersByPubkey,
|
||||
getFollows,
|
||||
getFollowers,
|
||||
follow,
|
||||
unfollow,
|
||||
tagPubkey,
|
||||
@@ -25,6 +23,7 @@
|
||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||
import UserPlus from "@assets/icons/user-plus.svg?dataurl"
|
||||
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||
@@ -42,6 +41,7 @@
|
||||
import ProfilePinnedNotes from "@app/components/ProfilePinnedNotes.svelte"
|
||||
import ProfilePageNotes from "@app/components/ProfilePageNotes.svelte"
|
||||
import ProfilePageSpaces from "@app/components/ProfilePageSpaces.svelte"
|
||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||
import ProfileQrCode from "@app/components/ProfileQrCode.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import {deriveGroupList, getSpaceUrlsFromGroupList, userSpaceUrls} from "@app/groups"
|
||||
@@ -65,11 +65,6 @@
|
||||
const score = deriveUserWotScore(target)
|
||||
const encodedNpub = nip19.npubEncode(target)
|
||||
const isSelf = $derived($pubkey === target)
|
||||
const followerCount = $derived.by(() => {
|
||||
void $followersByPubkey
|
||||
|
||||
return getFollowers(target).length
|
||||
})
|
||||
const isFollowing = $derived.by(() => {
|
||||
void $followLists
|
||||
|
||||
@@ -77,8 +72,7 @@
|
||||
})
|
||||
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
||||
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
|
||||
const displayScore = $derived(isSelf ? followerCount : Math.round(clamp([0, 100], $score)))
|
||||
const spaceBadgeCount = $derived(isSelf ? spaceUrls.length : sharedSpaceUrls.length)
|
||||
const displayScore = $derived(Math.round(clamp([0, 100], $score)))
|
||||
const website = $derived($profile?.website?.replace(/^https?:\/\//, ""))
|
||||
const websiteHref = $derived(
|
||||
$profile?.website?.match(/^https?:\/\//)
|
||||
@@ -118,6 +112,8 @@
|
||||
pushModal(EventInfo, {event: $profile!.event})
|
||||
}
|
||||
|
||||
const startEdit = () => pushModal(ProfileEdit)
|
||||
|
||||
const openSettings = () => goto("/settings/profile")
|
||||
|
||||
const openSpaces = () => goto("/spaces")
|
||||
@@ -191,10 +187,19 @@
|
||||
<div class="-mt-8 sm:-mt-10">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||
<div class="w-fit shrink-0">
|
||||
<div class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200">
|
||||
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
|
||||
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
|
||||
</div>
|
||||
{#if isSelf}
|
||||
<Button
|
||||
class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200"
|
||||
onclick={startEdit}>
|
||||
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
|
||||
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200">
|
||||
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
|
||||
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-3 sm:gap-2 sm:pt-14">
|
||||
@@ -209,7 +214,7 @@
|
||||
{#if isSelf}
|
||||
<Button
|
||||
class="btn btn-primary btn-md flex-1 sm:btn-sm sm:flex-none"
|
||||
onclick={openSettings}>
|
||||
onclick={startEdit}>
|
||||
<Icon icon={PenNewSquare} size={4} />
|
||||
Edit profile
|
||||
</Button>
|
||||
@@ -243,6 +248,14 @@
|
||||
User Details
|
||||
</Button>
|
||||
</li>
|
||||
{#if isSelf}
|
||||
<li>
|
||||
<Button onclick={openSettings}>
|
||||
<Icon icon={Settings} />
|
||||
Account Settings
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -275,23 +288,16 @@
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0">
|
||||
<Icon icon={Shield} size={3} />
|
||||
{#if isSelf}
|
||||
{followerCount} {followerCount === 1 ? "follower" : "followers"}
|
||||
{:else}
|
||||
Trust score {displayScore}
|
||||
{/if}
|
||||
Trust score {displayScore}
|
||||
</span>
|
||||
{#if spaceBadgeCount > 0}
|
||||
{#if sharedSpaceUrls.length > 0}
|
||||
<button
|
||||
class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0"
|
||||
onclick={showSpacesTab}>
|
||||
<Icon icon={UsersGroup} size={3} />
|
||||
{spaceBadgeCount}
|
||||
{#if isSelf}
|
||||
{spaceBadgeCount === 1 ? "space" : "spaces"}
|
||||
{:else}
|
||||
shared {spaceBadgeCount === 1 ? "space" : "spaces"}
|
||||
{/if}
|
||||
{sharedSpaceUrls.length} shared {sharedSpaceUrls.length === 1
|
||||
? "space"
|
||||
: "spaces"}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -332,7 +338,8 @@
|
||||
<div class="col-3 sm:col-4">
|
||||
<ProfileInfo pubkey={target} />
|
||||
<div class="col-3 xl:hidden">
|
||||
{@render profileAside()}
|
||||
<ProfileTrust pubkey={target} />
|
||||
<ProfileSharedSpaces pubkey={target} limit={3} onViewAll={showSpacesTab} />
|
||||
</div>
|
||||
<ProfilePinnedNotes
|
||||
pubkey={target}
|
||||
@@ -367,13 +374,9 @@
|
||||
|
||||
<aside class="hidden w-80 shrink-0 xl:block xl:border-l xl:border-base-300 xl:pl-4">
|
||||
<div class="col-3">
|
||||
{@render profileAside()}
|
||||
<ProfileTrust pubkey={target} />
|
||||
<ProfileSharedSpaces pubkey={target} limit={3} onViewAll={showSpacesTab} />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet profileAside()}
|
||||
<ProfileTrust pubkey={target} {isSelf} />
|
||||
<ProfileSharedSpaces pubkey={target} {isSelf} limit={3} onViewAll={showSpacesTab} />
|
||||
{/snippet}
|
||||
|
||||
@@ -17,11 +17,6 @@
|
||||
|
||||
const {pubkey, editable = false}: Props = $props()
|
||||
|
||||
let element: Element | undefined = $state()
|
||||
let events: TrustedEvent[] = $state([])
|
||||
let buffer: TrustedEvent[] = []
|
||||
let exhausted = $state(false)
|
||||
|
||||
const ctrl = makeFeedController({
|
||||
useWindowing: true,
|
||||
feed: feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
|
||||
@@ -30,11 +25,12 @@
|
||||
buffer.push(event)
|
||||
}
|
||||
},
|
||||
onExhausted: () => {
|
||||
exhausted = true
|
||||
},
|
||||
})
|
||||
|
||||
let element: Element | undefined = $state()
|
||||
let events: TrustedEvent[] = $state([])
|
||||
let buffer: TrustedEvent[] = []
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
element: element!,
|
||||
@@ -64,15 +60,9 @@
|
||||
<div in:fly>
|
||||
<NoteItem {event} {editable} />
|
||||
</div>
|
||||
{:else}
|
||||
{#if exhausted}
|
||||
<p class="py-12 text-center text-sm opacity-75">No notes found for this profile.</p>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if !exhausted}
|
||||
<p class="center my-12 flex">
|
||||
<Spinner loading />
|
||||
</p>
|
||||
{/if}
|
||||
<p class="center my-12 flex">
|
||||
<Spinner loading />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {onMount} from "svelte"
|
||||
import {sortBy} from "@welshman/lib"
|
||||
import {getListTags, getEventTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {derivePinList, repository} from "@welshman/app"
|
||||
import {Router} from "@welshman/router"
|
||||
import {load} from "@welshman/net"
|
||||
import {deriveEventsById, deriveEventsDesc} from "@welshman/store"
|
||||
import {fly} from "@lib/transition"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import NoteItem from "@app/components/NoteItem.svelte"
|
||||
@@ -21,87 +20,64 @@
|
||||
const {pubkey, limit, onViewAll, editable = false}: Props = $props()
|
||||
|
||||
const pinList = derivePinList(pubkey)
|
||||
const pinnedIds = $derived(getEventTagValues(getListTags($pinList)))
|
||||
const displayIds = $derived(limit ? pinnedIds.slice(0, limit) : pinnedIds)
|
||||
|
||||
const pinnedIds = derived(pinList, $pinList => getEventTagValues(getListTags($pinList)))
|
||||
const pinnedEvents = $derived.by(() => {
|
||||
return sortBy(
|
||||
e => -pinnedIds.indexOf(e.id),
|
||||
displayIds
|
||||
.map(id => repository.getEvent(id))
|
||||
.filter((event): event is TrustedEvent => Boolean(event)),
|
||||
)
|
||||
})
|
||||
|
||||
const displayIds = derived(pinnedIds, $pinnedIds =>
|
||||
limit ? $pinnedIds.slice(0, limit) : $pinnedIds,
|
||||
)
|
||||
let loading = $state(pinnedIds.length > 0)
|
||||
|
||||
const pinnedEvents = derived(
|
||||
displayIds,
|
||||
($displayIds, set) => {
|
||||
if ($displayIds.length === 0) {
|
||||
set([])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return deriveEventsDesc(
|
||||
deriveEventsById({repository, filters: [{ids: $displayIds}]}),
|
||||
).subscribe(events => {
|
||||
set(sortBy(event => -$displayIds.indexOf(event.id), events))
|
||||
})
|
||||
},
|
||||
[] as TrustedEvent[],
|
||||
)
|
||||
|
||||
let fetching = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
const ids = $displayIds
|
||||
|
||||
if (ids.length === 0) {
|
||||
fetching = false
|
||||
onMount(() => {
|
||||
if (pinnedIds.length === 0) {
|
||||
loading = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const missing = ids.filter(id => !repository.getEvent(id))
|
||||
const missing = pinnedIds.filter(id => !repository.getEvent(id))
|
||||
|
||||
if (missing.length === 0) {
|
||||
fetching = false
|
||||
loading = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fetching = true
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
load({
|
||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
||||
filters: [{ids: missing}],
|
||||
signal: controller.signal,
|
||||
onEvent: () => {
|
||||
loading = !pinnedIds.every(id => repository.getEvent(id))
|
||||
},
|
||||
onClose: () => {
|
||||
fetching = false
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
})
|
||||
|
||||
const loading = $derived(
|
||||
fetching || ($displayIds.length > 0 && $pinnedEvents.length < $displayIds.length),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if $displayIds.length > 0 || loading}
|
||||
{#if pinnedIds.length > 0}
|
||||
<div class="col-4 border-t border-base-300 pt-4">
|
||||
<strong>Pinned notes</strong>
|
||||
{#if loading && $pinnedEvents.length === 0}
|
||||
{#if loading && pinnedEvents.length === 0}
|
||||
<p class="center flex py-8">
|
||||
<Spinner loading />
|
||||
</p>
|
||||
{:else if $pinnedEvents.length > 0}
|
||||
{:else}
|
||||
<div class="col-2">
|
||||
{#each $pinnedEvents as event (event.id)}
|
||||
{#each pinnedEvents as event (event.id)}
|
||||
<div in:fly>
|
||||
<NoteItem {event} {editable} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if onViewAll && limit && $pinnedIds.length > limit}
|
||||
{#if onViewAll && pinnedIds.length > (limit || pinnedIds.length)}
|
||||
<button class="link link-primary row-2 text-sm" onclick={onViewAll}>
|
||||
View all pinned notes
|
||||
<span aria-hidden="true">→</span>
|
||||
|
||||
@@ -16,28 +16,26 @@
|
||||
|
||||
type Props = {
|
||||
pubkey: string
|
||||
isSelf?: boolean
|
||||
limit?: number
|
||||
onViewAll?: () => void
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {pubkey, isSelf = false, limit, onViewAll, ...props}: Props = $props()
|
||||
const {pubkey, limit, onViewAll, ...props}: Props = $props()
|
||||
|
||||
const groupList = deriveGroupList(pubkey)
|
||||
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
||||
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
|
||||
const listedSpaceUrls = $derived(isSelf ? spaceUrls : sharedSpaceUrls)
|
||||
const displayUrls = $derived(limit ? listedSpaceUrls.slice(0, limit) : listedSpaceUrls)
|
||||
const displayUrls = $derived(limit ? sharedSpaceUrls.slice(0, limit) : sharedSpaceUrls)
|
||||
</script>
|
||||
|
||||
<div class={cx("card2 bg-alt col-3 border border-base-300 max-sm:p-5 sm:col-4", props.class)}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="row-2">
|
||||
<Icon icon={UsersGroup} size={5} />
|
||||
<strong>{isSelf ? "Your spaces" : "Shared spaces"}</strong>
|
||||
<strong>Shared spaces</strong>
|
||||
</div>
|
||||
<span class="badge badge-neutral">{listedSpaceUrls.length}</span>
|
||||
<span class="badge badge-neutral">{sharedSpaceUrls.length}</span>
|
||||
</div>
|
||||
{#if displayUrls.length > 0}
|
||||
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
|
||||
@@ -60,17 +58,17 @@
|
||||
</Link>
|
||||
{/each}
|
||||
</div>
|
||||
{#if onViewAll && listedSpaceUrls.length > (limit || listedSpaceUrls.length)}
|
||||
{#if onViewAll && sharedSpaceUrls.length > (limit || sharedSpaceUrls.length)}
|
||||
<button
|
||||
class="link link-primary row-2 border-t border-base-300 pt-4 text-sm max-sm:pt-4"
|
||||
onclick={onViewAll}>
|
||||
{isSelf ? "View all your spaces" : "View all shared spaces"}
|
||||
View all shared spaces
|
||||
<Icon icon={AltArrowRight} size={4} />
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="border-t border-base-300 pt-4 text-sm opacity-75 max-sm:pt-4">
|
||||
{isSelf ? "You aren't in any spaces yet." : "No shared spaces yet."}
|
||||
No shared spaces yet.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
import {
|
||||
pubkey,
|
||||
followLists,
|
||||
userFollowList,
|
||||
deriveUserWotScore,
|
||||
deriveProfileDisplay,
|
||||
deriveFollowList,
|
||||
followersByPubkey,
|
||||
loadFollowList,
|
||||
getFollows,
|
||||
getFollowers,
|
||||
} from "@welshman/app"
|
||||
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -18,40 +14,18 @@
|
||||
|
||||
type Props = {
|
||||
pubkey: string
|
||||
isSelf?: boolean
|
||||
}
|
||||
|
||||
const {pubkey: target, isSelf = false}: Props = $props()
|
||||
const {pubkey: target}: Props = $props()
|
||||
|
||||
const score = deriveUserWotScore(target)
|
||||
const profileDisplay = deriveProfileDisplay(target)
|
||||
const targetFollowList = deriveFollowList(target)
|
||||
|
||||
$effect(() => {
|
||||
if (isSelf) return
|
||||
|
||||
loadFollowList(target)
|
||||
|
||||
const viewer = $pubkey
|
||||
|
||||
if (viewer) {
|
||||
loadFollowList(viewer)
|
||||
}
|
||||
})
|
||||
|
||||
const followerCount = $derived.by(() => {
|
||||
void $followersByPubkey
|
||||
|
||||
return getFollowers(target).length
|
||||
})
|
||||
|
||||
const mutualFollows = $derived.by(() => {
|
||||
if (isSelf) return []
|
||||
|
||||
const viewer = $pubkey
|
||||
void $followLists
|
||||
void $targetFollowList
|
||||
void $userFollowList
|
||||
|
||||
if (!viewer) return []
|
||||
|
||||
@@ -61,17 +35,10 @@
|
||||
getFollows(target).filter(pk => pk !== viewer && pk !== target && viewerFollows.has(pk)),
|
||||
)
|
||||
})
|
||||
|
||||
const displayScore = $derived(isSelf ? followerCount : Math.round(clamp([0, 100], $score)))
|
||||
const progress = $derived(isSelf ? undefined : displayScore)
|
||||
const displayScore = $derived(Math.round(clamp([0, 100], $score)))
|
||||
const progress = $derived(displayScore)
|
||||
|
||||
const trustMessage = $derived.by(() => {
|
||||
if (isSelf) {
|
||||
if (followerCount === 0) return "No one follows you in the network we know about yet."
|
||||
|
||||
return `${followerCount} ${followerCount === 1 ? "person follows" : "people follow"} you on the network we know about.`
|
||||
}
|
||||
|
||||
if (displayScore >= 70) return "This user is highly trusted in your network."
|
||||
if (displayScore >= 30) return "This user has some trust in your network."
|
||||
|
||||
@@ -86,18 +53,10 @@
|
||||
</div>
|
||||
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
|
||||
<div class="flex items-end justify-between gap-2">
|
||||
<span class="text-sm opacity-75">{isSelf ? "Followers" : "Trust score"}</span>
|
||||
<span class="text-lg font-semibold">
|
||||
{#if isSelf}
|
||||
{displayScore}
|
||||
{:else}
|
||||
{displayScore} / 100
|
||||
{/if}
|
||||
</span>
|
||||
<span class="text-sm opacity-75">Trust score</span>
|
||||
<span class="text-lg font-semibold">{displayScore} / 100</span>
|
||||
</div>
|
||||
{#if !isSelf}
|
||||
<progress class="progress progress-primary w-full" value={progress} max="100"></progress>
|
||||
{/if}
|
||||
<progress class="progress progress-primary w-full" value={progress} max="100"></progress>
|
||||
<p class="text-sm opacity-75">{trustMessage}</p>
|
||||
</div>
|
||||
{#if mutualFollows.length > 0}
|
||||
|
||||
@@ -25,6 +25,7 @@ const staticTitles = new Map<string, string>([
|
||||
["/chat", "Messages"],
|
||||
["/join", "Join Space"],
|
||||
["/people", "Find People"],
|
||||
["/people/[npub]", "Profile"],
|
||||
["/settings/about", "About"],
|
||||
["/settings/profile", "Profile Settings"],
|
||||
["/settings/content", "Content Settings"],
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {get} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import type {MakeNonOptional} from "@welshman/lib"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {ROOMS, NOTE, FOLLOWS} from "@welshman/util"
|
||||
import {
|
||||
loadProfile,
|
||||
loadRelayList,
|
||||
loadFollowList,
|
||||
loadMessagingRelayList,
|
||||
loadPinList,
|
||||
pubkey as sessionPubkey,
|
||||
} from "@welshman/app"
|
||||
import {load} from "@welshman/net"
|
||||
import {Router} from "@welshman/router"
|
||||
import {ROOMS, NOTE} from "@welshman/util"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -45,28 +42,19 @@
|
||||
await loadProfile(pubkey)
|
||||
await loadRelayList(pubkey)
|
||||
|
||||
const viewer = get(sessionPubkey)
|
||||
|
||||
await Promise.all([
|
||||
loadFollowList(pubkey),
|
||||
loadPinList(pubkey),
|
||||
loadGroupList(pubkey),
|
||||
loadMessagingRelayList(pubkey),
|
||||
viewer && viewer !== pubkey ? loadFollowList(viewer) : undefined,
|
||||
])
|
||||
|
||||
const filters: Filter[] = [
|
||||
{authors: [pubkey], kinds: [ROOMS]},
|
||||
{authors: [pubkey], kinds: [NOTE], limit: 1},
|
||||
]
|
||||
|
||||
if (get(sessionPubkey) === pubkey) {
|
||||
filters.push({kinds: [FOLLOWS], "#p": [pubkey], limit: 500})
|
||||
}
|
||||
|
||||
load({
|
||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
||||
filters,
|
||||
filters: [
|
||||
{authors: [pubkey], kinds: [ROOMS]},
|
||||
{authors: [pubkey], kinds: [NOTE], limit: 1},
|
||||
],
|
||||
})
|
||||
|
||||
ready = true
|
||||
|
||||
Reference in New Issue
Block a user