Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chats): unreads #685

Merged
merged 13 commits into from
Oct 11, 2024
9 changes: 5 additions & 4 deletions src/lib/components/messaging/ChatPreview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
import ProfilePictureMany from "../profile/ProfilePictureMany.svelte"
import { Store } from "$lib/state/Store"
import { goto } from "$app/navigation"
import { get } from "svelte/store"
import { derived, get } from "svelte/store"
import { tempCDN } from "$lib/utils/CommonVariables"
import { UIStore } from "$lib/state/ui"
import { _ } from "svelte-i18n"
import { checkMobile } from "$lib/utils/Mobile"
import { ConversationStore } from "$lib/state/conversation"
import { SettingsStore } from "$lib/state"

export let chat: Chat
export let cta: boolean = false
export let simpleUnreads: boolean = false
export let loading: boolean

const timeAgo = new TimeAgo("en-US")
Expand All @@ -28,6 +28,7 @@
$: loading = chatName === "Unknown User" || ($users.length <= 2 && ($users[1]?.loading == true || $users[0].loading == true))
$: directChatPhoto = $users[1]?.profile.photo.image ?? $users[0].profile.photo.image
$: chatStatus = $users.length > 2 ? Status.Offline : ($users[1]?.profile.status ?? $users[0].profile.status)
$: simpleUnreads = derived(SettingsStore.state, s => s.messaging.simpleUnreads)

let timeago = getTimeAgo(chat.last_message_at)
const dispatch = createEventDispatcher()
Expand Down Expand Up @@ -98,11 +99,11 @@
{timeago}
</Text>
{#if !loading}
{#if chat.notifications > 0 && !simpleUnreads}
{#if chat.notifications > 0 && !$simpleUnreads}
<span class="unreads">
{chat.notifications}
</span>
{:else if chat.notifications > 0 && simpleUnreads}
{:else if chat.notifications > 0 && $simpleUnreads}
<span class="unreads simple"></span>
{/if}
{/if}
Expand Down
94 changes: 86 additions & 8 deletions src/lib/components/messaging/Conversation.svelte
Original file line number Diff line number Diff line change
@@ -1,42 +1,92 @@
<script lang="ts">
import { afterUpdate, onMount } from "svelte"
import { afterUpdate, onDestroy, onMount } from "svelte"
import Button from "$lib/elements/Button.svelte"
import { Icon, Text, Label } from "$lib/elements"
import { ProfilePicture } from "$lib/components"
import { Appearance, Shape } from "$lib/enums"
import { fade } from "svelte/transition"
import { SettingsStore } from "$lib/state"
import { get } from "svelte/store"
import MessageSkeleton from "./message/MessageSkeleton.svelte"
import { derived, get } from "svelte/store"
import { _, date, time } from "svelte-i18n"
import { Store } from "$lib/state/Store"
import { UIStore } from "$lib/state/ui"

let scrollContainer: Element

let scrolledUp: boolean = false
let showScrollToBottom: boolean = false
let clearUnreads = true
export let loading: boolean = false
export let unreads: { unread: number; since: Date; last_viewed: string } | undefined

let lastUnread: { unread: number; since: Date; last_viewed: string } | undefined
$: chat = Store.state.activeChat
let setup: boolean = false
$: derived(chat, _ => {
if (setup) {
if (scrollContainer.scrollHeight <= scrollContainer.clientHeight) markAsRead($chat.id)
}
})
const scrollToBottom = (node: Element) => {
if (node) node.scrollTop = node.scrollHeight
}

function scrollCheck(threshold: number) {
return scrollContainer.scrollHeight - scrollContainer.scrollTop > scrollContainer.clientHeight * threshold
}

const handleScroll = () => {
const isScrolledUp = scrollContainer.scrollHeight - scrollContainer.scrollTop > scrollContainer.clientHeight * 1.5
showScrollToBottom = isScrolledUp
if (!setup) return
showScrollToBottom = scrollCheck(1.5)
let current = scrolledUp
scrolledUp = scrollCheck(1.1)
if (current != scrolledUp && !scrolledUp && unreads && unreads.unread > 0) {
// Clear unreads if scrolled to the bottom
if (clearUnreads) markAsRead($chat.id)
clearUnreads = true
}
}

const compact: boolean = get(SettingsStore.state).messaging.compact

afterUpdate(() => {
if (!showScrollToBottom) scrollToBottom(scrollContainer)
if (!scrolledUp) {
scrollToBottom(scrollContainer)
}
// Mark as read current is already read and messages are incoming
if (setup && lastUnread !== unreads && lastUnread === undefined) {
if (scrolledUp) clearUnreads = false
else markAsRead($chat.id)
}
lastUnread = unreads
})

onMount(() => {
// setTimeout(() => {
// loading = false
// }, 3000)
setTimeout(() => {
scrollToBottom(scrollContainer)
if (scrollContainer) {
if (unreads) {
let element = document.getElementById(`message-${unreads.last_viewed}`)
if (element) element.scrollIntoView({ behavior: "smooth" })
} else {
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}
setup = true
}, 250)
})

function markAsRead(chat: string) {
UIStore.mutateChat(chat, c => {
c.last_view_date = new Date()
c.notifications = 0
})
}

onDestroy(() => {
if (scrollContainer.scrollHeight <= scrollContainer.clientHeight) markAsRead($chat.id)
})
</script>

<div class={`conversation ${compact ? "compact" : ""}`}>
Expand All @@ -47,6 +97,12 @@
<Text class="min-text" loading={true} />
</div>
{:else}
{#if unreads && unreads.unread > 0}
<div class="unreads">
<div class="bookmark"></div>
{$_("chat.newMessageSinceAmount", { values: { amount: unreads.unread, date: $date(unreads.since, { format: "medium" }), time: $time(unreads.since) } })}
</div>
{/if}
<div bind:this={scrollContainer} class="scroll" on:scroll={handleScroll}>
<div class="spacer"></div>
<slot></slot>
Expand Down Expand Up @@ -85,6 +141,28 @@
justify-content: center;
}

.unreads {
position: absolute;
right: 0;
top: 0;
text-align: center;
padding: var(--padding-minimal);
padding-left: calc(var(--padding-minimal) + 30px);
background-color: var(--focus-color);
border-radius: 0 0 var(--border-radius) var(--border-radius);
z-index: 1;
.bookmark {
position: absolute;
top: 0;
left: 0;
height: 60px;
-webkit-transform: rotate(0deg) skew(0deg);
transform: rotate(0deg) skew(0deg);
border-left: 15px solid color-mix(in srgb, var(--focus-color) 40%, #000000);
border-right: 15px solid color-mix(in srgb, var(--focus-color) 40%, #000000);
border-bottom: 15px solid transparent;
}
}
.scroll {
display: flex;
align-items: flex-end;
Expand Down
8 changes: 6 additions & 2 deletions src/lib/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@
"show-participants": "Participants",
"group-settings": "Settings",
"replyTo": "Replying to: {user}",
"attachments-count": "Attachments: {amount}"
"attachments-count": "Attachments: {amount}",
"newMessageSince": "New messages since {time} - {date}",
"newMessageSinceAmount": "{amount} new messages since {time} - {date}"
},
"community": {
"title": "Satellite Community - General",
Expand Down Expand Up @@ -427,7 +429,9 @@
"quick": "Quick Chat",
"quickDescription": "When navigating back to the chats screen on mobile, quick chat will bring you back to your last conversation.",
"showStatusWidgets": "Widget Panel",
"showStatusWidgetsDescription": "Enable the widget panel, which displays system information and other helpful data."
"showStatusWidgetsDescription": "Enable the widget panel, which displays system information and other helpful data.",
"simpleUnreads": "Simple Unreads",
"simpleUnreadsDescription": "Whether to show a simple unread badge for chats in the sidebar or not"
},
"preferences": {
"appLanguage": "App Language",
Expand Down
3 changes: 3 additions & 0 deletions src/lib/layouts/Chatbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
let result = replyTo ? await RaygunStoreInstance.reply(chat.id, replyTo.id, txt) : await RaygunStoreInstance.send(get(Store.state.activeChat).id, text.split("\n"), attachments)

result.onSuccess(res => {
UIStore.mutateChat(chat.id, c => {
c.last_view_date = new Date()
})
ConversationStore.addPendingMessages(chat.id, res.message, txt)
})
if (!isStickerOrGif) {
Expand Down
30 changes: 0 additions & 30 deletions src/lib/state/conversation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ export type ConversationMessages = {
messages: MessageGroup[]
}

export type Unreads = {
id: string
count: number
}

class Conversations {
/**
* INTERNAL!
Expand All @@ -29,12 +24,10 @@ class Conversations {
private conversations: Writable<ConversationMessagesMap>
// We use a new writable so they dont get saved to db
pendingMsgConversations: Writable<{ [conversation: string]: { [id: string]: PendingMessage } }>
unreads: Writable<Unreads[]>

constructor() {
this.conversationsDB = []
this.conversations = writable({})
this.unreads = writable([])
this.pendingMsgConversations = writable({})
this.loadConversations()
this.conversations.subscribe(async convsStore => {
Expand Down Expand Up @@ -425,29 +418,6 @@ class Conversations {
// },
// })
}

addUnread(chat: string) {
const unreads = get(this.unreads)
const index = unreads.findIndex(u => u.id === chat)
if (index !== -1) {
unreads[index].count++
} else {
unreads.push({
id: chat,
count: 1,
})
}
this.unreads.set(unreads)
}

clearUnreads(chat: string) {
const unreads = get(this.unreads)
const index = unreads.findIndex(u => u.id === chat)
if (index !== -1) {
unreads[index].count = 0
}
this.unreads.set(unreads)
}
}

export const ConversationStore = new Conversations()
1 change: 1 addition & 0 deletions src/lib/state/settings/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export let defaultSettings = {
compact: false,
quick: false,
identiconStyle: Identicon.BotsNeutral,
simpleUnreads: true,
},
audio: {
inputDevice: "Default",
Expand Down
1 change: 1 addition & 0 deletions src/lib/state/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ISettingsState {
compact: boolean
quick: boolean
identiconStyle: Identicon
simpleUnreads: boolean
}
audio: {
inputDevice: string
Expand Down
7 changes: 5 additions & 2 deletions src/lib/state/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { TypingIndicator, type Chat, type FontOption } from "$lib/types"
import { derived, get, writable, type Writable } from "svelte/store"
import { createPersistentState } from ".."
import { EmojiFont, Font, Identicon } from "$lib/enums"
import { EmojiFont, Font, Identicon, Route } from "$lib/enums"
import { Store as MainStore } from "../Store"
import { mchats } from "$lib/mock/users"
import { page } from "$app/stores"

export interface IUIState {
color: Writable<string>
Expand All @@ -17,6 +18,7 @@ export interface IUIState {
sidebarOpen: Writable<boolean>
chats: Writable<Chat[]>
hiddenChats: Writable<Chat[]>
simpleUnreads: Writable<boolean>
emojiSelector: Writable<boolean>
emojiCounter: Writable<{ [emoji: string]: number }>
marketOpen: Writable<boolean>
Expand Down Expand Up @@ -45,6 +47,7 @@ class Store {
},
}),
hiddenChats: createPersistentState("uplink.ui.hiddenChats", []),
simpleUnreads: writable(true),
emojiSelector: writable(false),
emojiCounter: createPersistentState("uplink.ui.emojiCounter", { "👍": 0, "👎": 0, "❤️": 0, "🖖": 0, "😂": 0 }),
marketOpen: writable(false),
Expand Down Expand Up @@ -142,7 +145,7 @@ class Store {
}

addNotification(conversationId: string) {
if (get(MainStore.state.activeChat).id !== conversationId) {
if (get(page).route.id !== Route.Chat || get(MainStore.state.activeChat).id !== conversationId) {
this.mutateChat(conversationId, chat => {
chat.notifications++
})
Expand Down
2 changes: 2 additions & 0 deletions src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export type Chat = {
settings: ChatSettings
creator?: string
notifications: number
last_view_date: Date
users: string[]
typing_indicator: TypingIndicator
last_message_id: string
Expand Down Expand Up @@ -282,6 +283,7 @@ export let defaultChat: Chat = {
motd: "",
unread: 0,
notifications: 0,
last_view_date: new Date(),
kind: ChatType.DirectMessage,
creator: undefined,
settings: {
Expand Down
1 change: 1 addition & 0 deletions src/lib/wasm/RaygunStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ class RaygunStore {
settings.audio.messageSounds ? Sounds.Notification : undefined
)
}
UIStore.addNotification(conversation_id)
//TODO move chat to top
//TODO handle ping
}
Expand Down
Loading