feat: auto-load poll results
This commit is contained in:
@@ -3,14 +3,14 @@ import { POLL_TYPE } from '@/constants'
|
|||||||
import { useFetchPollResults } from '@/hooks/useFetchPollResults'
|
import { useFetchPollResults } from '@/hooks/useFetchPollResults'
|
||||||
import { createPollResponseDraftEvent } from '@/lib/draft-event'
|
import { createPollResponseDraftEvent } from '@/lib/draft-event'
|
||||||
import { getPollMetadataFromEvent } from '@/lib/event-metadata'
|
import { getPollMetadataFromEvent } from '@/lib/event-metadata'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, isPartiallyInViewport } from '@/lib/utils'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import pollResultsService from '@/services/poll-results.service'
|
import pollResultsService from '@/services/poll-results.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { CheckCircle2, Loader2 } from 'lucide-react'
|
import { CheckCircle2, Loader2 } from 'lucide-react'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
@@ -32,6 +32,30 @@ export default function Poll({ event, className }: { event: Event; className?: s
|
|||||||
const isExpired = useMemo(() => poll?.endsAt && dayjs().unix() > poll.endsAt, [poll])
|
const isExpired = useMemo(() => poll?.endsAt && dayjs().unix() > poll.endsAt, [poll])
|
||||||
const isMultipleChoice = useMemo(() => poll?.pollType === POLL_TYPE.MULTIPLE_CHOICE, [poll])
|
const isMultipleChoice = useMemo(() => poll?.pollType === POLL_TYPE.MULTIPLE_CHOICE, [poll])
|
||||||
const canVote = useMemo(() => !isExpired && !votedOptionIds.length, [isExpired, votedOptionIds])
|
const canVote = useMemo(() => !isExpired && !votedOptionIds.length, [isExpired, votedOptionIds])
|
||||||
|
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pollResults || isLoadingResults || !containerElement) return
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isPartiallyInViewport(containerElement)) {
|
||||||
|
fetchResults()
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.observe(containerElement)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(containerElement)
|
||||||
|
}
|
||||||
|
}, [pollResults, isLoadingResults, containerElement])
|
||||||
|
|
||||||
if (!poll) {
|
if (!poll) {
|
||||||
return null
|
return null
|
||||||
@@ -102,12 +126,21 @@ export default function Poll({ event, className }: { event: Event; className?: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className} ref={setContainerElement}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{poll.pollType === POLL_TYPE.MULTIPLE_CHOICE && (
|
<p>
|
||||||
<p>{t('Multiple choice (select one or more)')}</p>
|
{poll.pollType === POLL_TYPE.MULTIPLE_CHOICE &&
|
||||||
)}
|
t('Multiple choice (select one or more)')}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{!!poll.endsAt &&
|
||||||
|
(isExpired
|
||||||
|
? t('Poll has ended')
|
||||||
|
: t('Poll ends at {{time}}', {
|
||||||
|
time: new Date(poll.endsAt * 1000).toLocaleString()
|
||||||
|
}))}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Poll Options */}
|
{/* Poll Options */}
|
||||||
@@ -115,7 +148,7 @@ export default function Poll({ event, className }: { event: Event; className?: s
|
|||||||
{poll.options.map((option) => {
|
{poll.options.map((option) => {
|
||||||
const votes = pollResults?.results?.[option.id]?.size ?? 0
|
const votes = pollResults?.results?.[option.id]?.size ?? 0
|
||||||
const totalVotes = pollResults?.totalVotes ?? 0
|
const totalVotes = pollResults?.totalVotes ?? 0
|
||||||
const percentage = totalVotes > 0 ? (votes / totalVotes) * 100 : 0
|
const percentage = !canVote && totalVotes > 0 ? (votes / totalVotes) * 100 : 0
|
||||||
const isMax =
|
const isMax =
|
||||||
pollResults && pollResults.totalVotes > 0
|
pollResults && pollResults.totalVotes > 0
|
||||||
? Object.values(pollResults.results).every((res) => res.size <= votes)
|
? Object.values(pollResults.results).every((res) => res.size <= votes)
|
||||||
@@ -148,7 +181,7 @@ export default function Poll({ event, className }: { event: Event; className?: s
|
|||||||
<CheckCircle2 className="size-4 shrink-0" />
|
<CheckCircle2 className="size-4 shrink-0" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!!pollResults && (
|
{!canVote && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground shrink-0 z-10',
|
'text-muted-foreground shrink-0 z-10',
|
||||||
@@ -173,49 +206,37 @@ export default function Poll({ event, className }: { event: Event; className?: s
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results Summary */}
|
{/* Results Summary */}
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="flex justify-between items-center text-sm text-muted-foreground">
|
||||||
{!!pollResults && t('{{number}} votes', { number: pollResults.totalVotes ?? 0 })}
|
<div>{t('{{number}} votes', { number: pollResults?.totalVotes ?? 0 })}</div>
|
||||||
{!!pollResults && !!poll.endsAt && ' · '}
|
|
||||||
{!!poll.endsAt &&
|
{isLoadingResults && t('Loading...')}
|
||||||
(isExpired
|
{!isLoadingResults && !canVote && (
|
||||||
? t('Poll has ended')
|
<div
|
||||||
: t('Poll ends at {{time}}', {
|
className="hover:underline cursor-pointer"
|
||||||
time: new Date(poll.endsAt * 1000).toLocaleString()
|
onClick={(e) => {
|
||||||
}))}
|
e.stopPropagation()
|
||||||
|
fetchResults()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!pollResults ? t('Load results') : t('Refresh results')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(canVote || !pollResults) && (
|
{/* Vote Button */}
|
||||||
<div className="flex items-center justify-between gap-2">
|
{canVote && !!selectedOptionIds.length && (
|
||||||
{/* Vote Button */}
|
<Button
|
||||||
{canVote && (
|
onClick={(e) => {
|
||||||
<Button
|
e.stopPropagation()
|
||||||
onClick={(e) => {
|
if (selectedOptionIds.length === 0) return
|
||||||
e.stopPropagation()
|
handleVote()
|
||||||
if (selectedOptionIds.length === 0) return
|
}}
|
||||||
handleVote()
|
disabled={!selectedOptionIds.length || isVoting}
|
||||||
}}
|
className="w-full"
|
||||||
disabled={!selectedOptionIds.length || isVoting}
|
>
|
||||||
className="flex-1"
|
{isVoting && <Loader2 className="animate-spin" />}
|
||||||
>
|
{t('Vote')}
|
||||||
{isVoting && <Loader2 className="animate-spin" />}
|
</Button>
|
||||||
{t('Vote')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!pollResults && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
fetchResults()
|
|
||||||
}}
|
|
||||||
disabled={isLoadingResults}
|
|
||||||
>
|
|
||||||
{isLoadingResults && <Loader2 className="animate-spin" />}
|
|
||||||
{t('Load results')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': 'فشل إلغاء المتابعة',
|
'Unfollow failed': 'فشل إلغاء المتابعة',
|
||||||
'show new notes': 'إظهار الملاحظات الجديدة',
|
'show new notes': 'إظهار الملاحظات الجديدة',
|
||||||
'loading...': 'جار التحميل...',
|
'loading...': 'جار التحميل...',
|
||||||
|
'Loading...': 'جار التحميل...',
|
||||||
'no more notes': 'لا توجد ملاحظات إضافية',
|
'no more notes': 'لا توجد ملاحظات إضافية',
|
||||||
'reply to': 'الرد على',
|
'reply to': 'الرد على',
|
||||||
reply: 'رد',
|
reply: 'رد',
|
||||||
@@ -310,6 +311,7 @@ export default {
|
|||||||
'End Date (optional)': 'تاريخ الانتهاء (اختياري)',
|
'End Date (optional)': 'تاريخ الانتهاء (اختياري)',
|
||||||
'Clear end date': 'مسح تاريخ الانتهاء',
|
'Clear end date': 'مسح تاريخ الانتهاء',
|
||||||
'Relay URLs (optional, comma-separated)': 'عناوين المرحلات (اختياري، مفصولة بفواصل)',
|
'Relay URLs (optional, comma-separated)': 'عناوين المرحلات (اختياري، مفصولة بفواصل)',
|
||||||
'Remove poll': 'إزالة الاستطلاع'
|
'Remove poll': 'إزالة الاستطلاع',
|
||||||
|
'Refresh results': 'تحديث النتائج'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': 'Nicht mehr folgen fehlgeschlagen',
|
'Unfollow failed': 'Nicht mehr folgen fehlgeschlagen',
|
||||||
'show new notes': 'zeige neue Notizen',
|
'show new notes': 'zeige neue Notizen',
|
||||||
'loading...': 'lädt...',
|
'loading...': 'lädt...',
|
||||||
|
'Loading...': 'Lade...',
|
||||||
'no more notes': 'keine weiteren Notizen',
|
'no more notes': 'keine weiteren Notizen',
|
||||||
'reply to': 'antworten an',
|
'reply to': 'antworten an',
|
||||||
reply: 'antworten',
|
reply: 'antworten',
|
||||||
@@ -317,6 +318,7 @@ export default {
|
|||||||
'End Date (optional)': 'Enddatum (optional)',
|
'End Date (optional)': 'Enddatum (optional)',
|
||||||
'Clear end date': 'Enddatum löschen',
|
'Clear end date': 'Enddatum löschen',
|
||||||
'Relay URLs (optional, comma-separated)': 'Relay-URLs (optional, durch Kommas getrennt)',
|
'Relay URLs (optional, comma-separated)': 'Relay-URLs (optional, durch Kommas getrennt)',
|
||||||
'Remove poll': 'Umfrage entfernen'
|
'Remove poll': 'Umfrage entfernen',
|
||||||
|
'Refresh results': 'Ergebnisse aktualisieren'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': 'Unfollow failed',
|
'Unfollow failed': 'Unfollow failed',
|
||||||
'show new notes': 'show new notes',
|
'show new notes': 'show new notes',
|
||||||
'loading...': 'loading...',
|
'loading...': 'loading...',
|
||||||
|
'Loading...': 'Loading...',
|
||||||
'no more notes': 'no more notes',
|
'no more notes': 'no more notes',
|
||||||
'reply to': 'reply to',
|
'reply to': 'reply to',
|
||||||
reply: 'reply',
|
reply: 'reply',
|
||||||
@@ -310,6 +311,7 @@ export default {
|
|||||||
'End Date (optional)': 'End Date (optional)',
|
'End Date (optional)': 'End Date (optional)',
|
||||||
'Clear end date': 'Clear end date',
|
'Clear end date': 'Clear end date',
|
||||||
'Relay URLs (optional, comma-separated)': 'Relay URLs (optional, comma-separated)',
|
'Relay URLs (optional, comma-separated)': 'Relay URLs (optional, comma-separated)',
|
||||||
'Remove poll': 'Remove poll'
|
'Remove poll': 'Remove poll',
|
||||||
|
'Refresh results': 'Refresh results'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': 'Error al dejar de seguir',
|
'Unfollow failed': 'Error al dejar de seguir',
|
||||||
'show new notes': 'mostrar nuevas notas',
|
'show new notes': 'mostrar nuevas notas',
|
||||||
'loading...': 'cargando...',
|
'loading...': 'cargando...',
|
||||||
|
'Loading...': 'Cargando...',
|
||||||
'no more notes': 'no hay más notas',
|
'no more notes': 'no hay más notas',
|
||||||
'reply to': 'responder a',
|
'reply to': 'responder a',
|
||||||
reply: 'responder',
|
reply: 'responder',
|
||||||
@@ -315,6 +316,7 @@ export default {
|
|||||||
'End Date (optional)': 'Fecha de finalización (opcional)',
|
'End Date (optional)': 'Fecha de finalización (opcional)',
|
||||||
'Clear end date': 'Borrar fecha de finalización',
|
'Clear end date': 'Borrar fecha de finalización',
|
||||||
'Relay URLs (optional, comma-separated)': 'URLs de relé (opcional, separadas por comas)',
|
'Relay URLs (optional, comma-separated)': 'URLs de relé (opcional, separadas por comas)',
|
||||||
'Remove poll': 'Eliminar encuesta'
|
'Remove poll': 'Eliminar encuesta',
|
||||||
|
'Refresh results': 'Actualizar resultados'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': 'لغو دنبال کردن ناموفق',
|
'Unfollow failed': 'لغو دنبال کردن ناموفق',
|
||||||
'show new notes': 'نمایش یادداشتهای جدید',
|
'show new notes': 'نمایش یادداشتهای جدید',
|
||||||
'loading...': 'در حال بارگذاری...',
|
'loading...': 'در حال بارگذاری...',
|
||||||
|
'Loading...': 'در حال بارگذاری...',
|
||||||
'no more notes': 'یادداشت بیشتری وجود ندارد',
|
'no more notes': 'یادداشت بیشتری وجود ندارد',
|
||||||
'reply to': 'پاسخ به',
|
'reply to': 'پاسخ به',
|
||||||
reply: 'پاسخ',
|
reply: 'پاسخ',
|
||||||
@@ -312,6 +313,7 @@ export default {
|
|||||||
'End Date (optional)': 'تاریخ پایان (اختیاری)',
|
'End Date (optional)': 'تاریخ پایان (اختیاری)',
|
||||||
'Clear end date': 'پاک کردن تاریخ پایان',
|
'Clear end date': 'پاک کردن تاریخ پایان',
|
||||||
'Relay URLs (optional, comma-separated)': 'آدرسهای رله (اختیاری، جدا شده با کاما)',
|
'Relay URLs (optional, comma-separated)': 'آدرسهای رله (اختیاری، جدا شده با کاما)',
|
||||||
'Remove poll': 'حذف نظرسنجی'
|
'Remove poll': 'حذف نظرسنجی',
|
||||||
|
'Refresh results': 'بارگیری مجدد نتایج'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': "Échec de l'arrêt du suivi",
|
'Unfollow failed': "Échec de l'arrêt du suivi",
|
||||||
'show new notes': 'afficher les nouvelles notes',
|
'show new notes': 'afficher les nouvelles notes',
|
||||||
'loading...': 'chargement...',
|
'loading...': 'chargement...',
|
||||||
|
'Loading...': 'Chargement...',
|
||||||
'no more notes': 'plus de notes',
|
'no more notes': 'plus de notes',
|
||||||
'reply to': 'répondre à',
|
'reply to': 'répondre à',
|
||||||
reply: 'répondre',
|
reply: 'répondre',
|
||||||
@@ -316,6 +317,7 @@ export default {
|
|||||||
'Clear end date': 'Effacer la date de fin',
|
'Clear end date': 'Effacer la date de fin',
|
||||||
'Relay URLs (optional, comma-separated)':
|
'Relay URLs (optional, comma-separated)':
|
||||||
'URLs de relais (optionnel, séparées par des virgules)',
|
'URLs de relais (optionnel, séparées par des virgules)',
|
||||||
'Remove poll': 'Supprimer le sondage'
|
'Remove poll': 'Supprimer le sondage',
|
||||||
|
'Refresh results': 'Rafraîchir les résultats'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': 'Disiscrizione non riuscita',
|
'Unfollow failed': 'Disiscrizione non riuscita',
|
||||||
'show new notes': 'mostra nuove note',
|
'show new notes': 'mostra nuove note',
|
||||||
'loading...': 'caricando...',
|
'loading...': 'caricando...',
|
||||||
|
'Loading...': 'Caricamento in corso...',
|
||||||
'no more notes': 'basta note',
|
'no more notes': 'basta note',
|
||||||
'reply to': 'replica a',
|
'reply to': 'replica a',
|
||||||
reply: 'replica',
|
reply: 'replica',
|
||||||
@@ -314,6 +315,7 @@ export default {
|
|||||||
'End Date (optional)': 'Data di fine (opzionale)',
|
'End Date (optional)': 'Data di fine (opzionale)',
|
||||||
'Clear end date': 'Cancella data di fine',
|
'Clear end date': 'Cancella data di fine',
|
||||||
'Relay URLs (optional, comma-separated)': 'URL relay (opzionale, separati da virgole)',
|
'Relay URLs (optional, comma-separated)': 'URL relay (opzionale, separati da virgole)',
|
||||||
'Remove poll': 'Rimuovi sondaggio'
|
'Remove poll': 'Rimuovi sondaggio',
|
||||||
|
'Refresh results': 'Aggiorna risultati'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': 'フォロー解除に失敗しました',
|
'Unfollow failed': 'フォロー解除に失敗しました',
|
||||||
'show new notes': '新しいノートを表示',
|
'show new notes': '新しいノートを表示',
|
||||||
'loading...': '読み込み中...',
|
'loading...': '読み込み中...',
|
||||||
|
'Loading...': '読み込み中...',
|
||||||
'no more notes': 'これ以上ノートはありません',
|
'no more notes': 'これ以上ノートはありません',
|
||||||
'reply to': '返信先',
|
'reply to': '返信先',
|
||||||
reply: '返信',
|
reply: '返信',
|
||||||
@@ -312,6 +313,7 @@ export default {
|
|||||||
'End Date (optional)': '終了日(任意)',
|
'End Date (optional)': '終了日(任意)',
|
||||||
'Clear end date': '終了日をクリア',
|
'Clear end date': '終了日をクリア',
|
||||||
'Relay URLs (optional, comma-separated)': 'リレーURL(任意、カンマ区切り)',
|
'Relay URLs (optional, comma-separated)': 'リレーURL(任意、カンマ区切り)',
|
||||||
'Remove poll': '投票を削除'
|
'Remove poll': '投票を削除',
|
||||||
|
'Refresh results': '結果を更新'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': '언팔로우 실패',
|
'Unfollow failed': '언팔로우 실패',
|
||||||
'show new notes': '새 노트 보기',
|
'show new notes': '새 노트 보기',
|
||||||
'loading...': '로딩 중...',
|
'loading...': '로딩 중...',
|
||||||
|
'Loading...': '로딩 중...',
|
||||||
'no more notes': '더 이상 노트 없음',
|
'no more notes': '더 이상 노트 없음',
|
||||||
'reply to': '답글',
|
'reply to': '답글',
|
||||||
reply: '답글',
|
reply: '답글',
|
||||||
@@ -312,6 +313,7 @@ export default {
|
|||||||
'End Date (optional)': '종료 날짜 (선택사항)',
|
'End Date (optional)': '종료 날짜 (선택사항)',
|
||||||
'Clear end date': '종료 날짜 지우기',
|
'Clear end date': '종료 날짜 지우기',
|
||||||
'Relay URLs (optional, comma-separated)': '릴레이 URL (선택사항, 쉼표로 구분)',
|
'Relay URLs (optional, comma-separated)': '릴레이 URL (선택사항, 쉼표로 구분)',
|
||||||
'Remove poll': '투표 제거'
|
'Remove poll': '투표 제거',
|
||||||
|
'Refresh results': '결과 새로 고침'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': 'Porzucenie obserwacji nieudane',
|
'Unfollow failed': 'Porzucenie obserwacji nieudane',
|
||||||
'show new notes': 'Pokaż nowe wpisy',
|
'show new notes': 'Pokaż nowe wpisy',
|
||||||
'loading...': 'ładowanie...',
|
'loading...': 'ładowanie...',
|
||||||
|
'Loading...': 'Ładowanie...',
|
||||||
'no more notes': 'Koniec wpisów',
|
'no more notes': 'Koniec wpisów',
|
||||||
'reply to': 'Odpowiedź na',
|
'reply to': 'Odpowiedź na',
|
||||||
reply: 'odpowiedz',
|
reply: 'odpowiedz',
|
||||||
@@ -314,6 +315,7 @@ export default {
|
|||||||
'Clear end date': 'Wyczyść datę zakończenia',
|
'Clear end date': 'Wyczyść datę zakończenia',
|
||||||
'Relay URLs (optional, comma-separated)':
|
'Relay URLs (optional, comma-separated)':
|
||||||
'Adresy URL przekaźników (opcjonalne, oddzielone przecinkami)',
|
'Adresy URL przekaźników (opcjonalne, oddzielone przecinkami)',
|
||||||
'Remove poll': 'Usuń ankietę'
|
'Remove poll': 'Usuń ankietę',
|
||||||
|
'Refresh results': 'Odśwież wyniki'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': 'Falha ao deixar de seguir',
|
'Unfollow failed': 'Falha ao deixar de seguir',
|
||||||
'show new notes': 'Ver novas notas',
|
'show new notes': 'Ver novas notas',
|
||||||
'loading...': 'Carregando...',
|
'loading...': 'Carregando...',
|
||||||
|
'Loading...': 'Carregando...',
|
||||||
'no more notes': 'Não há mais notas',
|
'no more notes': 'Não há mais notas',
|
||||||
'reply to': 'Respondendo a',
|
'reply to': 'Respondendo a',
|
||||||
reply: 'Responder',
|
reply: 'Responder',
|
||||||
@@ -313,6 +314,7 @@ export default {
|
|||||||
'End Date (optional)': 'Data de término (opcional)',
|
'End Date (optional)': 'Data de término (opcional)',
|
||||||
'Clear end date': 'Limpar data de término',
|
'Clear end date': 'Limpar data de término',
|
||||||
'Relay URLs (optional, comma-separated)': 'URLs de relay (opcional, separadas por vírgulas)',
|
'Relay URLs (optional, comma-separated)': 'URLs de relay (opcional, separadas por vírgulas)',
|
||||||
'Remove poll': 'Remover enquete'
|
'Remove poll': 'Remover enquete',
|
||||||
|
'Refresh results': 'Atualizar resultados'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': 'Falha ao Deixar de Seguir',
|
'Unfollow failed': 'Falha ao Deixar de Seguir',
|
||||||
'show new notes': 'mostrar novas notas',
|
'show new notes': 'mostrar novas notas',
|
||||||
'loading...': 'carregando...',
|
'loading...': 'carregando...',
|
||||||
|
'Loading...': 'Carregando...',
|
||||||
'no more notes': 'não há mais notas',
|
'no more notes': 'não há mais notas',
|
||||||
'reply to': 'responder a',
|
'reply to': 'responder a',
|
||||||
reply: 'responder',
|
reply: 'responder',
|
||||||
@@ -314,6 +315,7 @@ export default {
|
|||||||
'End Date (optional)': 'Data de fim (opcional)',
|
'End Date (optional)': 'Data de fim (opcional)',
|
||||||
'Clear end date': 'Limpar data de fim',
|
'Clear end date': 'Limpar data de fim',
|
||||||
'Relay URLs (optional, comma-separated)': 'URLs de relay (opcional, separadas por vírgulas)',
|
'Relay URLs (optional, comma-separated)': 'URLs de relay (opcional, separadas por vírgulas)',
|
||||||
'Remove poll': 'Remover sondagem'
|
'Remove poll': 'Remover sondagem',
|
||||||
|
'Refresh results': 'Atualizar resultados'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'Unfollow failed': 'Ошибка отписки',
|
'Unfollow failed': 'Ошибка отписки',
|
||||||
'show new notes': 'показать новые заметки',
|
'show new notes': 'показать новые заметки',
|
||||||
'loading...': 'загрузка...',
|
'loading...': 'загрузка...',
|
||||||
|
'Loading...': 'Загрузка...',
|
||||||
'no more notes': 'больше нет заметок',
|
'no more notes': 'больше нет заметок',
|
||||||
'reply to': 'ответить',
|
'reply to': 'ответить',
|
||||||
reply: 'ответить',
|
reply: 'ответить',
|
||||||
@@ -315,6 +316,7 @@ export default {
|
|||||||
'End Date (optional)': 'Дата окончания (необязательно)',
|
'End Date (optional)': 'Дата окончания (необязательно)',
|
||||||
'Clear end date': 'Очистить дату окончания',
|
'Clear end date': 'Очистить дату окончания',
|
||||||
'Relay URLs (optional, comma-separated)': 'URL релеев (необязательно, через запятую)',
|
'Relay URLs (optional, comma-separated)': 'URL релеев (необязательно, через запятую)',
|
||||||
'Remove poll': 'Удалить опрос'
|
'Remove poll': 'Удалить опрос',
|
||||||
|
'Refresh results': 'Обновить результаты'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': 'เลิกติดตามไม่สำเร็จ',
|
'Unfollow failed': 'เลิกติดตามไม่สำเร็จ',
|
||||||
'show new notes': 'แสดงโน้ตใหม่',
|
'show new notes': 'แสดงโน้ตใหม่',
|
||||||
'loading...': 'กำลังโหลด...',
|
'loading...': 'กำลังโหลด...',
|
||||||
|
'Loading...': 'กำลังโหลด...',
|
||||||
'no more notes': 'ไม่มีโน้ตเพิ่มเติม',
|
'no more notes': 'ไม่มีโน้ตเพิ่มเติม',
|
||||||
'reply to': 'ตอบกลับถึง',
|
'reply to': 'ตอบกลับถึง',
|
||||||
reply: 'ตอบกลับ',
|
reply: 'ตอบกลับ',
|
||||||
@@ -309,6 +310,7 @@ export default {
|
|||||||
'End Date (optional)': 'วันที่สิ้นสุด (ไม่บังคับ)',
|
'End Date (optional)': 'วันที่สิ้นสุด (ไม่บังคับ)',
|
||||||
'Clear end date': 'ล้างวันที่สิ้นสุด',
|
'Clear end date': 'ล้างวันที่สิ้นสุด',
|
||||||
'Relay URLs (optional, comma-separated)': 'URL รีเลย์ (ไม่บังคับ, คั่นด้วยจุลภาค)',
|
'Relay URLs (optional, comma-separated)': 'URL รีเลย์ (ไม่บังคับ, คั่นด้วยจุลภาค)',
|
||||||
'Remove poll': 'ลบโพลล์'
|
'Remove poll': 'ลบโพลล์',
|
||||||
|
'Refresh results': 'รีเฟรชผลลัพธ์'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
'Unfollow failed': '取消关注失败',
|
'Unfollow failed': '取消关注失败',
|
||||||
'show new notes': '显示新笔记',
|
'show new notes': '显示新笔记',
|
||||||
'loading...': '加载中...',
|
'loading...': '加载中...',
|
||||||
|
'Loading...': '加载中...',
|
||||||
'no more notes': '到底了',
|
'no more notes': '到底了',
|
||||||
'reply to': '回复',
|
'reply to': '回复',
|
||||||
reply: '回复',
|
reply: '回复',
|
||||||
@@ -310,6 +311,7 @@ export default {
|
|||||||
'End Date (optional)': '结束日期(可选)',
|
'End Date (optional)': '结束日期(可选)',
|
||||||
'Clear end date': '清除结束日期',
|
'Clear end date': '清除结束日期',
|
||||||
'Relay URLs (optional, comma-separated)': '中继服务器 URL(可选,逗号分隔)',
|
'Relay URLs (optional, comma-separated)': '中继服务器 URL(可选,逗号分隔)',
|
||||||
'Remove poll': '移除投票'
|
'Remove poll': '移除投票',
|
||||||
|
'Refresh results': '刷新结果'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ export function isInViewport(el: HTMLElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPartiallyInViewport(el: HTMLElement) {
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
return (
|
||||||
|
rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.bottom > 0 &&
|
||||||
|
rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
|
||||||
|
rect.right > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function isEmail(email: string) {
|
export function isEmail(email: string) {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ExtendedKind } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import { getPollResponseFromEvent } from '@/lib/event-metadata'
|
import { getPollResponseFromEvent } from '@/lib/event-metadata'
|
||||||
|
import DataLoader from 'dataloader'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Filter } from 'nostr-tools'
|
import { Filter } from 'nostr-tools'
|
||||||
import client from './client.service'
|
import client from './client.service'
|
||||||
@@ -15,6 +16,56 @@ class PollResultsService {
|
|||||||
static instance: PollResultsService
|
static instance: PollResultsService
|
||||||
private pollResultsMap: Map<string, TPollResults> = new Map()
|
private pollResultsMap: Map<string, TPollResults> = new Map()
|
||||||
private pollResultsSubscribers = new Map<string, Set<() => void>>()
|
private pollResultsSubscribers = new Map<string, Set<() => void>>()
|
||||||
|
private loader = new DataLoader<
|
||||||
|
{
|
||||||
|
pollEventId: string
|
||||||
|
relays: string[]
|
||||||
|
validPollOptionIds: string[]
|
||||||
|
isMultipleChoice: boolean
|
||||||
|
endsAt?: number
|
||||||
|
},
|
||||||
|
TPollResults | undefined
|
||||||
|
>(
|
||||||
|
async (params) => {
|
||||||
|
const pollMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
relays: string[]
|
||||||
|
validPollOptionIds: string[]
|
||||||
|
isMultipleChoice: boolean
|
||||||
|
endsAt?: number
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
|
params.forEach(({ pollEventId, relays, validPollOptionIds, isMultipleChoice, endsAt }) => {
|
||||||
|
if (!pollMap.has(pollEventId)) {
|
||||||
|
pollMap.set(pollEventId, { relays, validPollOptionIds, isMultipleChoice, endsAt })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pollResults = await Promise.allSettled(
|
||||||
|
Array.from(pollMap).map(async ([pollEventId, pollParams]) => {
|
||||||
|
const result = await this._fetchResults(
|
||||||
|
pollEventId,
|
||||||
|
pollParams.relays,
|
||||||
|
pollParams.validPollOptionIds,
|
||||||
|
pollParams.isMultipleChoice,
|
||||||
|
pollParams.endsAt
|
||||||
|
)
|
||||||
|
return { pollEventId, result }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const resultMap = new Map<string, TPollResults>()
|
||||||
|
pollResults.forEach((promiseResult) => {
|
||||||
|
if (promiseResult.status === 'fulfilled' && promiseResult.value.result) {
|
||||||
|
resultMap.set(promiseResult.value.pollEventId, promiseResult.value.result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return params.map(({ pollEventId }) => resultMap.get(pollEventId))
|
||||||
|
},
|
||||||
|
{ cache: false }
|
||||||
|
)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!PollResultsService.instance) {
|
if (!PollResultsService.instance) {
|
||||||
@@ -30,6 +81,23 @@ class PollResultsService {
|
|||||||
isMultipleChoice: boolean,
|
isMultipleChoice: boolean,
|
||||||
endsAt?: number
|
endsAt?: number
|
||||||
) {
|
) {
|
||||||
|
return this.loader.load({
|
||||||
|
pollEventId,
|
||||||
|
relays,
|
||||||
|
validPollOptionIds,
|
||||||
|
isMultipleChoice,
|
||||||
|
endsAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchResults(
|
||||||
|
pollEventId: string,
|
||||||
|
relays: string[],
|
||||||
|
validPollOptionIds: string[],
|
||||||
|
isMultipleChoice: boolean,
|
||||||
|
endsAt?: number
|
||||||
|
) {
|
||||||
|
console.log('Fetching poll results for:', pollEventId)
|
||||||
const filter: Filter = {
|
const filter: Filter = {
|
||||||
kinds: [ExtendedKind.POLL_RESPONSE],
|
kinds: [ExtendedKind.POLL_RESPONSE],
|
||||||
'#e': [pollEventId],
|
'#e': [pollEventId],
|
||||||
|
|||||||
Reference in New Issue
Block a user