dev - wip channel pagination

This commit is contained in:
slatinsky 2024-08-04 18:22:31 +02:00
parent f0fcd614a2
commit 0a99ee694d
16 changed files with 1069 additions and 226 deletions

View file

@ -100,58 +100,141 @@ async def get_roles(guild_id: str):
return list(cursor)
@app.get("/message-ids")
async def get_message_ids(channel_id: str, guild_id: str):
def sort_ids_asc(ids: list):
"""
Returns a list of message ids for a channel.
Sort ids in ascending order.
Because sometimes we are querying messages in descending order, we need to reverse the list.
"""
return sorted(ids, key=lambda x: int(x))
@app.get("/message-ids-paginated")
async def get_message_ids(channel_id: str, guild_id: str, message_id: str = None, direction="around", limit=100):
"""
Returns a subset of message ids for a channel.
User supplies a message id, for which we return a subset of message ids around it
- special message id "first" returns the first messages
- special message id "last" returns the last messages
Direction can be "around", "before" or "after"
- "around" returns messages before and after the message id
- requires message id
- "before" returns messages before the message id
- requires message id
- "after" returns messages after the message id
- requires message id
Message id can be 24 long id or special id "first" or "last"
The endpoint returns special message ids "first" and "last" if there are more messages to load in that direction.
no cache
"""
if not message_id:
raise Exception("Message id is required")
if message_id != 'first' and message_id != 'last':
message_id = pad_id(message_id)
channel_id = pad_id(channel_id)
guild_id = pad_id(guild_id)
limit = int(limit)
collection_messages = get_guild_collection(guild_id, "messages")
if is_compiled():
cache_dir = "../../storage/cache/message-ids"
else:
cache_dir = "../../release/dcef/storage/cache/message-ids"
cache_path = f"{cache_dir}/{channel_id}.json"
denylisted_user_ids_path = f"{cache_dir}/denylisted_user_ids.json"
ids_before = []
ids_after = []
# clear entire cache if denylisted user ids changed
denylisted_user_ids = get_denylisted_user_ids()
if os.path.exists(cache_path):
with open(denylisted_user_ids_path, "r", encoding="utf-8") as f:
file_content = f.read()
if file_content != str(denylisted_user_ids):
shutil.rmtree(cache_dir)
os.makedirs(cache_dir)
if direction == "around" and (message_id != "first" and message_id != "last"):
limit = limit // 2
# read cached ids if available
if os.path.exists(cache_path):
print("get_message_ids() cache hit - channel id", channel_id)
with open(cache_path, "r", encoding="utf-8") as f:
file_content = f.read()
return json.loads(file_content)
print("get_message_ids() cache miss - channel id", channel_id)
query = {
"channelId": channel_id,
"author._id": {
"$nin": denylisted_user_ids
if message_id == "first":
query = {
"channelId": channel_id,
"author._id": {
"$nin": denylisted_user_ids
}
}
}
ids = collection_messages.find(query, {"_id": 1}).sort([("_id", pymongo.ASCENDING)])
new_ids = [str(id["_id"]) for id in ids]
ids_before = ['first']
ids_after = collection_messages.find(query, {"_id": 1}).sort([("_id", pymongo.ASCENDING)]).limit(limit)
ids_after = [str(id["_id"]) for id in ids_after]
if len(ids_after) != limit:
ids_after.append('last')
ids = ids_before + ids_after
elif message_id == "last":
query = {
"channelId": channel_id,
"author._id": {
"$nin": denylisted_user_ids
}
}
ids_after = ['last']
ids_before = collection_messages.find(query, {"_id": 1}).sort([("_id", pymongo.DESCENDING)]).limit(limit)
ids_before = sort_ids_asc([str(id["_id"]) for id in ids_before])
if len(ids_before) != limit:
ids_before.insert(0, 'first')
ids = ids_before + ids_after
else:
if direction == "around" or direction == "before":
ids_before = collection_messages.find(
{
"channelId": channel_id,
"_id": {
"$lt": message_id
},
"author._id": {
"$nin": denylisted_user_ids
}
},
{"_id": 1}
).sort([("_id", pymongo.DESCENDING)]).limit(limit)
ids_before = sort_ids_asc([str(id["_id"]) for id in ids_before])
if direction == "around" or direction == "after":
ids_after = collection_messages.find(
{
"channelId": channel_id,
"_id": {
"$gt": message_id
},
"author._id": {
"$nin": denylisted_user_ids
}
},
{"_id": 1}
).sort([("_id", pymongo.ASCENDING)]).limit(limit)
ids_after = [str(id["_id"]) for id in ids_after]
if direction == "around":
if len(ids_before) != limit:
ids_before.insert(0, 'first')
if len(ids_after) != limit:
ids_after.append('last')
if len(ids_before) + len(ids_after) > 2: # found anything?
ids = ids_before + [message_id] + ids_after
else:
ids = ids_before + ids_after
elif direction == "before":
if len(ids_before) != limit:
ids_before.insert(0, 'first')
ids = ids_before
elif direction == "after":
if len(ids_after) != limit:
ids_after.append('last')
ids = ids_after
else:
raise Exception("Invalid direction")
return ids
# save to cache
file_content = re.sub(r"'", '"', str(new_ids))
with open(cache_path, "w", encoding="utf-8") as f:
f.write(file_content)
file_content = re.sub(r"'", '"', str(denylisted_user_ids))
with open(denylisted_user_ids_path, "w", encoding="utf-8") as f:
f.write(file_content)
return new_ids
class MessageRequest(BaseModel):

View file

@ -17,7 +17,7 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"svelte": "^5.0.0-next.144",
"svelte": "^5.0.0-next.148",
"svelte-check": "^3.6.7",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
@ -1631,9 +1631,9 @@
}
},
"node_modules/svelte": {
"version": "5.0.0-next.144",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.144.tgz",
"integrity": "sha512-akjtRBHzaLa1XdMv9tBGkXE5N2JaRc3gL+ZIctjc9Gew9DF7NxGTlxXq+HR9yUV7Lsg4o9ltMfkxz8H3K7piNQ==",
"version": "5.0.0-next.148",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.148.tgz",
"integrity": "sha512-4N1V1BjClRsXBFOUKQZxmwg0Xb62ZUEM0B/5Q9bfeuR37uBpqkv4HYzso/QNqwVYPmnOmeMzQkrHJcuwh+ZCsQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",

View file

@ -12,7 +12,7 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"svelte": "^5.0.0-next.144",
"svelte": "^5.0.0-next.148",
"svelte-check": "^3.6.7",
"tslib": "^2.6.2",
"typescript": "^5.2.2",

View file

@ -18,8 +18,13 @@ async function _fetchMessagesFromApi(guildId: string | null, messageIds: string[
if (guildId === null) {
guildId = "000000000000000000000000"
}
messageIds = messageIds.filter((messageId) => {
return messageId !== "first" && messageId !== "last";
})
let notInCache = messageIds.filter((messageId) => {
return !messageCacheWithoutReference[messageId]
return !messageCacheWithoutReference[messageId];
})
if (notInCache.length > 0) {
@ -44,6 +49,8 @@ async function _fetchMessagesFromApi(guildId: string | null, messageIds: string[
export async function messsageIdsToMessages(guildId: string, messageIds: string[]) {
const containsFirst = messageIds.includes("first");
const containsLast = messageIds.includes("last");
console.log("api - fetching " + messageIds.length + " messages");
if (messageIds.length === 0) {
return [];
@ -80,5 +87,12 @@ export async function messsageIdsToMessages(guildId: string, messageIds: string[
outMessages.push(messageCopy);
}
if (containsFirst) {
outMessages.unshift({_id: "first"});
}
if (containsLast) {
outMessages.push({_id: "last"});
}
return outMessages;
}

View file

@ -1,11 +1,11 @@
import type { Category, Channel } from "../interfaces";
export async function fetchMessageIds(guildId: string | null, channelId: string) {
export async function fetchMessageIds(guildId: string | null, channelId: string, direction: "before" | "after" | "around" | "first" | "last", messageId: string | null = null, limit: number = 50) {
if (guildId === null) {
guildId = "000000000000000000000000"
}
try {
let response = await fetch(`/api/message-ids?guild_id=${encodeURIComponent(guildId)}&channel_id=${encodeURIComponent(channelId)}`)
let response = await fetch(`/api/message-ids-paginated?guild_id=${encodeURIComponent(guildId)}&channel_id=${encodeURIComponent(channelId)}&direction=${direction}&message_id=${encodeURIComponent(messageId)}&limit=${limit}`)
let messageIds = await response.json()
return messageIds
}

View file

@ -150,6 +150,20 @@ export function getGuildState() {
console.log("router - replaced", state);
}
async function fetchChannelPinnedMessagesIds() {
if (!guildId || !channelId) {
return []
}
channelPinnedMessagesIds = await fetchPinnedMessageIds(guildId, channelId)
}
async function fetchThreadPinnedMessagesIds() {
if (!guildId || !threadId) {
return []
}
threadPinnedMessagesIds = await fetchPinnedMessageIds(guildId, threadId)
}
async function changeGuildId(newGuildId: string | null) {
if (newGuildId === "000000000000000000000000") {
newGuildId = null
@ -159,40 +173,99 @@ export function getGuildState() {
}
guildId = newGuildId;
searchState.clearSearch()
await changeChannelId(null)
await changeChannelId(null, null)
categories = await fetchCategoriesChannelsThreads(guildId)
console.log("router - changed guildId", guildId);
}
async function changeChannelId(newChannelId: string | null) {
if (channelId === newChannelId) {
async function changeChannelId(newChannelId: string | null, newChannelMessageId: string | null) {
if (channelId === newChannelId && channelMessageId === newChannelMessageId) {
return;
}
if (channelId !== newChannelId) {
await changeThreadId(null, null)
layoutState.hideChannelPinned()
channelPinnedMessagesIds = []
}
channelId = newChannelId;
await changeThreadId(null)
if (channelMessageId !== newChannelMessageId) {
channelMessageId = newChannelMessageId
}
if (newChannelId) {
channelMessagesIds = await fetchMessageIds(guildId, newChannelId)
channelMessageId = channelMessagesIds.length > 0 ? channelMessagesIds[-1] : null // last message
channelPinnedMessagesIds = await fetchPinnedMessageIds(guildId, newChannelId)
if (newChannelMessageId === "last") {
channelMessagesIds = await fetchMessageIds(guildId, newChannelId, "last", newChannelMessageId, 50)
}
else if (newChannelMessageId === "first") {
channelMessagesIds = await fetchMessageIds(guildId, newChannelId, "first", newChannelMessageId, 50)
}
else {
channelMessagesIds = await fetchMessageIds(guildId, newChannelId, "around", newChannelMessageId, 100)
}
if (channelMessageId === null) {
channelMessageId = channelMessagesIds[channelMessagesIds.length - 1] // last message
}
}
else {
channelMessagesIds = []
channelMessageId = null
channelPinnedMessagesIds = []
}
console.log("router - changed channelId", channelId);
console.log("router - changed channelId", channelId, "messageId", channelMessageId);
}
async function changeThreadId(newThreadId: string | null) {
if (threadId === newThreadId) {
async function loadChannelMessageIdsBefore() {
if (!channelId) {
return
}
if (channelMessagesIds.length === 0) {
return
}
const soonestMessageId = channelMessagesIds[0]
if (soonestMessageId === "first") {
return
}
const oldCount = channelMessagesIds.length
const newMessageIds = await fetchMessageIds(guildId, channelId, "before", soonestMessageId, 5)
channelMessagesIds = [...newMessageIds, ...channelMessagesIds ]
console.warn("router - loadChannelMessageIdsBefore - loaded", channelMessagesIds.length - oldCount, "new messages before", soonestMessageId);
}
async function loadChannelMessageIdsAfter() {
if (!channelId) {
return
}
if (channelMessagesIds.length === 0) {
return
}
const latestMessageId = channelMessagesIds[channelMessagesIds.length - 1]
let oldCount = channelMessagesIds.length
const newMessageIds = await fetchMessageIds(guildId, channelId, "after", latestMessageId, 5)
channelMessagesIds = [...channelMessagesIds, ...newMessageIds]
console.log("router - loadChannelMessageIdsAfter - loaded", channelMessagesIds.length - oldCount, "new messages after", latestMessageId);
}
async function changeThreadId(newThreadId: string | null, newThreadMessageId: string | null) {
if (threadId === newThreadId && threadMessageId === newThreadMessageId) {
return;
}
if (threadId !== newThreadId) {
layoutState.hideThreadPinned()
threadPinnedMessagesIds = []
}
threadId = newThreadId;
if (threadMessageId !== newThreadMessageId) {
threadMessageId = newThreadMessageId
}
if (newThreadId && guildId) {
threadMessagesIds = await fetchMessageIds(guildId, newThreadId)
threadMessageId = threadMessagesIds.length > 0 ? threadMessagesIds[-1] : null // last message
threadPinnedMessagesIds = await fetchPinnedMessageIds(guildId, newThreadId)
threadMessagesIds = await fetchMessageIds(guildId, newThreadId, "last", newThreadMessageId)
if (threadMessageId === null) {
threadMessageId = threadMessagesIds[threadMessagesIds.length - 1] // last message
}
layoutState.showThread()
}
@ -206,43 +279,6 @@ export function getGuildState() {
console.log("router - changed threadId", threadId);
}
async function changeChannelMessageId(newMessageId: string | null) {
let proposedChange
if (newMessageId === null) {
proposedChange = null
}
else if (channelMessagesIds.includes(newMessageId)) {
proposedChange = newMessageId
}
else {
console.error("router - changeChannelMessageId - message not found", newMessageId)
proposedChange = null
}
if (proposedChange !== channelMessageId) {
channelMessageId = proposedChange
console.log("router - changed channelMessageId", channelMessageId);
}
}
async function changeThreadMessageId(newMessageId: string | null) {
let proposedChange
if (newMessageId === null) {
proposedChange = null
}
else if (threadMessagesIds.includes(newMessageId)) {
proposedChange = newMessageId
}
else {
console.error("router - changeThreadMessageId - message not found", newMessageId)
proposedChange = null
}
if (proposedChange !== threadMessageId) {
threadMessageId = proposedChange
console.log("router - changed threadMessageId", threadMessageId);
}
}
/**
* Switch to guildId and channelOrThreadId (will be automatically detected if it's a channel or thread id)
@ -251,10 +287,10 @@ export function getGuildState() {
async function comboSetGuildChannel(guildId: string, channelOrThreadId: string) {
await changeGuildId(guildId)
if (isChannel(channelOrThreadId)) {
await changeChannelId(channelOrThreadId)
await changeChannelId(channelOrThreadId, null)
}
else if (isThread(channelOrThreadId)) {
await changeThreadId(channelOrThreadId)
await changeThreadId(channelOrThreadId, null)
}
else {
console.warn("router - comboSetGuildChannel - channel or thread not exported", channelOrThreadId)
@ -266,12 +302,12 @@ export function getGuildState() {
async function comboSetGuildChannelMessage(guildId: string, channelOrThreadId: string, messageId: string) {
await changeGuildId(guildId)
if (isChannel(channelOrThreadId)) {
await changeChannelId(channelOrThreadId)
await changeChannelMessageId(messageId)
await changeChannelId(channelOrThreadId, messageId)
// await changeChannelMessageId(messageId)
}
else if (isThread(channelOrThreadId)) {
await changeThreadId(channelOrThreadId)
await changeThreadMessageId(messageId)
await changeThreadId(channelOrThreadId, messageId)
// await changeThreadMessageId(messageId)
}
else {
console.warn("router - comboSetGuildChannelMessage - channel or thread not exported", channelOrThreadId)
@ -321,16 +357,18 @@ export function getGuildState() {
get threadPinnedMessagesIds() {
return threadPinnedMessagesIds;
},
loadChannelMessageIdsBefore,
loadChannelMessageIdsAfter,
changeGuildId,
changeChannelId,
changeThreadId,
changeChannelMessageId,
changeThreadMessageId,
comboSetGuildChannel,
comboSetGuildChannelMessage,
getUrlState,
pushState,
replaceState,
fetchChannelPinnedMessagesIds,
fetchThreadPinnedMessagesIds,
};
}
@ -353,10 +391,10 @@ export function channelOrThreadIdToName(channelId: string) {
async function restoreGuildState(state) {
await guildState.changeGuildId(state.guild);
await guildState.changeChannelId(state.channel);
await guildState.changeThreadId(state.thread);
await guildState.changeChannelMessageId(state.channelmessage);
await guildState.changeThreadMessageId(state.threadmessage);
await guildState.changeChannelId(state.channel, state.channelmessage);
await guildState.changeThreadId(state.thread, state.threadmessage);
// await guildState.changeChannelMessageId(state.channelmessage);
// await guildState.changeThreadMessageId(state.threadmessage);
await searchState.setSearchPrompt(state.search)
await searchState.search(guildState.guildId)
@ -388,13 +426,15 @@ window.addEventListener("popstate", async (e) => {
})
export function changeMessageId(channelOrThreadId: string, messageId: string) {
export async function changeMessageId(channelOrThreadId: string, messageId: string) {
if (isChannel(channelOrThreadId)) {
guildState.changeChannelMessageId(messageId)
await guildState.changeChannelId(channelOrThreadId, messageId);
// guildState.changeChannelMessageId(messageId)
guildState.pushState()
}
else if (isThread(channelOrThreadId)) {
guildState.changeThreadMessageId(messageId)
await guildState.changeThreadId(channelOrThreadId, messageId);
// guildState.changeThreadMessageId(messageId)
guildState.pushState()
}
else {

View file

@ -1,6 +1,8 @@
import { getSearchState } from "../../lib/search/searchState.svelte";
import { getGuildState } from "./guildState.svelte";
const searchState = getSearchState();
const guildState = getGuildState();
let mobilesidepanelshown = $state(false);
let searchManuallyHidden = $state(false);
@ -78,11 +80,35 @@ export function getLayoutState() {
}
}
function toggleChannelPinned() {
channelpinnedshown = !channelpinnedshown;
async function toggleChannelPinned() {
if (channelpinnedshown) {
hideChannelPinned()
}
else {
await showChannelPinned()
}
}
function toggleThreadPinned() {
threadpinnedshown = !threadpinnedshown;
function hideChannelPinned() {
channelpinnedshown = false;
}
async function showChannelPinned() {
channelpinnedshown = true;
await guildState.fetchChannelPinnedMessagesIds()
}
async function toggleThreadPinned() {
if (threadpinnedshown) {
hideThreadPinned()
}
else {
await showThreadPinned()
}
}
function hideThreadPinned() {
threadpinnedshown = false;
}
async function showThreadPinned() {
threadpinnedshown = true;
await guildState.fetchThreadPinnedMessagesIds()
}
return {
@ -133,7 +159,11 @@ export function getLayoutState() {
hideSettingsSideMenu,
toggleSettingsSideMenu,
toggleChannelPinned,
hideChannelPinned,
showChannelPinned,
toggleThreadPinned,
hideThreadPinned,
showThreadPinned,
};
}

View file

@ -3,19 +3,35 @@
import { getGuildState } from "../js/stores/guildState.svelte";
import { getLayoutState } from "../js/stores/layoutState.svelte";
import DateSeparator from "./DateSeparator.svelte";
import InfiniteScroll from "./InfiniteScroll.svelte";
import InfiniteScroll2 from "./InfiniteScroll2.svelte";
import ChannelStart from "./message/ChannelStart.svelte";
import Message from "./message/Message.svelte";
const guildState = getGuildState()
const layoutState = getLayoutState()
let apiGuildId = $derived(guildState.guildId ? guildState.guildId : "000000000000000000000000")
let apiChannelId = $derived(guildState.channelId)
export async function fetchMessageIds(direction: "before" | "after" | "around" | "first" | "last", messageId: string | null = null, limit: number) {
try {
let response = await fetch(`/api/message-ids-paginated?guild_id=${encodeURIComponent(apiGuildId)}&channel_id=${encodeURIComponent(apiChannelId)}&direction=${direction}&message_id=${encodeURIComponent(messageId)}&limit=${limit}`)
let messageIds = await response.json()
return messageIds
}
catch (e) {
console.error("api - Failed to fetch message ids", e)
return []
}
}
</script>
{#snippet renderMessageSnippet(index, message, previousMessage)}
{#if index === 0}
<!-- {#if index === 0}
<ChannelStart channelName={message.channelName} isThread={false} messageAuthor={message.author} />
{/if}
{/if} -->
{#if isDateDifferent(previousMessage, message)}
<DateSeparator messageId={message._id} />
@ -26,13 +42,58 @@
</div>
{/snippet}
{#snippet channelStartSnippet(index, message, previousMessage)}
<ChannelStart channelName={message.channelName} isThread={false} messageAuthor={message.author} />
{/snippet}
{#snippet channelEndSnippet(index, message, previousMessage)}
<div data-messageid="last">
this is the end of the channel
</div>
{/snippet}
{#snippet renderMessageSnippet2(message, previousMessage)}
<div data-messageid={message._id}>
{#if message._id === "first"}
<!-- <ChannelStart channelName={message.channelName} isThread={false} messageAuthor={message.author} /> -->
<div>channel start</div>
{:else if message._id === "last"}
<div>channel end</div>
{:else}
{#if isDateDifferent(previousMessage, message)}
<DateSeparator messageId={message._id} />
{/if}
<Message message={message} previousMessage={previousMessage} />
{/if}
</div>
{/snippet}
<div class="channel-wrapper" class:threadshown={layoutState.threadshown}>
<div class="channel" >
<!-- TODO: support change of selectedMessageId without rerender -->
{#key guildState.channelMessageId}
<InfiniteScroll debugname="channel" ids={guildState.channelMessagesIds} guildId={guildState.guildId} selectedMessageId={guildState.channelMessageId} renderMessageSnippet={renderMessageSnippet} bottomAligned={true} />
{/key}
{#if guildState.channelId !== null}
<!-- TODO: support change of selectedMessageId without rerender -->
{#key guildState.channelMessageId}
<!-- <InfiniteScroll
debugname="channel"
ids={guildState.channelMessagesIds}
loadBefore={guildState.loadChannelMessageIdsBefore}
loadAfter={guildState.loadChannelMessageIdsAfter}
guildId={guildState.guildId}
selectedMessageId={guildState.channelMessageId}
renderMessageSnippet={renderMessageSnippet}
channelStartSnippet={channelStartSnippet}
channelEndSnippet={channelEndSnippet}
bottomAligned={true}
/> -->
{#key apiChannelId}
<InfiniteScroll2
fetchMessageIds={fetchMessageIds}
guildId={apiGuildId}
snippetMessage={renderMessageSnippet2}
/>
{/key}
{/key}
{/if}
</div>
</div>

View file

@ -0,0 +1,287 @@
<script lang="ts">
import { untrack } from "svelte";
import type { Message } from "../js/interfaces";
// --------------------------
// THIS CODE IS NOT FINISHED
// TODO: needs backend pagination support to optimize loading
// --------------------------
import { messsageIdsToMessages } from "../js/messages";
interface MyProps {
ids: string[]
guildId: string
selectedMessageId: string | null
bottomAligned: boolean
debugname: string
loadBefore: () => Promise<void>
loadAfter: () => Promise<void>
renderMessageSnippet: (index: number, message: Message, previousMessage: Message) => void
channelStartSnippet: (index: number, message: Message, previousMessage: Message) => void
channelEndSnippet: (index: number, message: Message, previousMessage: Message) => void
}
let { ids, guildId, selectedMessageId = null, renderMessageSnippet, channelStartSnippet, channelEndSnippet, bottomAligned, debugname="", loadBefore, loadAfter}: MyProps = $props();
let maxMessages = 12000
let loadIncrement = 30
let startLoadingPixels = 500
let scrollContainer: HTMLDivElement
let messages: Message[] = $state([])
let lowestLoadedIndex: number | null = $state(null)
let highestLoadedIndex: number | null = $state(null)
let loadedIds: string[] = $state([])
let topReached = $derived(loadedIds.includes("first"))
let bottomReached = $derived(loadedIds.includes("last"))
let watchScroll: boolean = $state(false)
console.warn(`scroller[${debugname}] - RELOADED`, ids)
let isInitialLoad = true
async function refetchMessages(loadedIds: string[]) {
messages = await messsageIdsToMessages(guildId, loadedIds)
}
async function idsChanged() {
console.log(`scroller[${debugname}] - ids changed - selectedMessageId ${selectedMessageId} ids length ${ids.length}`)
if (ids.length === 0) {
console.log(`scroller[${debugname}] - no messages to load`)
messages = []
return
}
watchScroll = false
// let centerIndex = Math.round(ids.length / 2)
let centerIndex
if (selectedMessageId) {
centerIndex = ids.indexOf(selectedMessageId)
console.log(`scroller[${debugname}] - selected message index ${centerIndex}`)
}
else {
console.log(`scroller[${debugname}] - no selected message`)
}
if (centerIndex == null || centerIndex < 0) {
centerIndex = Math.round(ids.length - 1)
}
// highestLoadedIndex = centerIndex
// lowestLoadedIndex = centerIndex
// loadedIds = [ids[centerIndex]]
// for (let i = 1; i < maxMessages; i++) { // TODO: rewrite using slice
// if (centerIndex + i < ids.length) {
// highestLoadedIndex = centerIndex + i
// loadedIds = [...loadedIds, ids[highestLoadedIndex]]
// }
// if (centerIndex - i >= 0) {
// lowestLoadedIndex = centerIndex - i
// loadedIds = [ids[lowestLoadedIndex], ...loadedIds, ]
// }
// }
// loadedIds = [...ids]
loadedIds = ids
await refetchMessages(loadedIds)
if (isInitialLoad) {
isInitialLoad = false
setTimeout(async() => {
if (!scrollContainer) { // did we destroy the component before the timeout happened?
return
}
// scroll to selected message
if (selectedMessageId) {
let failedToScroll = true
// find selected message index
if (loadedIds.includes(selectedMessageId)) {
let selectedMessageElement = scrollContainer.querySelector(`[data-messageid="${selectedMessageId}"]`)
if (selectedMessageElement) {
selectedMessageElement.scrollIntoView({ block: "start", behavior: "instant" })
console.log(`scroller[${debugname}] - selected message scrolled into view`)
failedToScroll = false
}
}
if (failedToScroll) {
if (ids.includes(selectedMessageId)) {
console.warn(`scroller[${debugname}] - selected message not found in loaded messages BUT found in all messages`)
}
else {
console.warn(`scroller[${debugname}] - selected message not found in loaded messages and in all messages`)
}
}
}
else {
scrollContainer.scrollTop = scrollContainer.scrollHeight
console.log(`scroller[${debugname}] - no selected message to scroll to`)
}
watchScroll = true
}, 0)
}
// console.log(`scroller[${debugname}] - loadedIds`, loadedIds)
}
async function loadBeforeWrapper() {
console.log(`scroller[${debugname}] - loadBeforeWrapper before`, loadedIds.length)
console.log(`scroller[${debugname}] - loadBefore`, loadBefore)
await loadBefore()
// for (let i = 1; i < loadIncrement; i++) { // TODO: rewrite using slice
// if (!topReached) {
// // // add lowest
// // lowestLoadedIndex = lowestLoadedIndex - 1
// // loadedIds = [ids[lowestLoadedIndex], ...loadedIds]
// // // remove highest
// // if (loadedIds.length > maxMessages) {
// // highestLoadedIndex = highestLoadedIndex - 1
// // loadedIds = loadedIds.slice(0, -1)
// // }
// }
// else {
// console.log(`scroller[${debugname}] - top of the page - No more messages to load.`)
// break
// }
// }
// await refetchMessages(loadedIds)
console.log(`scroller[${debugname}] - loadBeforeWrapper after`, loadedIds.length)
watchScroll = true
}
async function loadAfterWrapper() {
console.log(`scroller[${debugname}] - loadAfterWrapper before`, loadedIds.length)
console.log(`scroller[${debugname}] - loadAfter`, loadAfter)
await loadAfter()
// for (let i = 1; i < loadIncrement; i++) { // TODO: rewrite using slice
// if (!bottomReached) {
// // // add highest
// // highestLoadedIndex = highestLoadedIndex + 1
// // loadedIds = [...loadedIds, ids[highestLoadedIndex]]
// // // remove lowest
// // if (loadedIds.length > maxMessages) {
// // lowestLoadedIndex = lowestLoadedIndex + 1
// // loadedIds = loadedIds.slice(1)
// // }
// }
// else {
// console.log(`scroller[${debugname}] - bottom of the page - No more messages to load.`)
// break
// }
// }
console.log(`scroller[${debugname}] - loadAfterWrapper after`, loadedIds.length)
// await refetchMessages(loadedIds)
watchScroll = true
}
// https://github.com/sveltejs/svelte/issues/9248#issuecomment-1732246774
function explicitEffect(fn, depsFn) {
$effect(() => {
depsFn();
untrack(fn);
});
}
// run idsChanged() only if ids change
explicitEffect(
() => {
idsChanged()
},
() => [ids]
);
async function handleScroll(event) {
if (!watchScroll)
return
if (messages.length === 0)
return
// console.log(`scroller[${debugname}] - handleScroll`, JSON.parse(JSON.stringify(loadedIds)))
// console.log(`scroller[${debugname}] - handleScroll`, loadedIds.length)
const element = event.target
let margin = startLoadingPixels
// console.log(`scroller[${debugname}] - scrollTop ${element.scrollTop}`)
if (!topReached && element.scrollTop < margin) {
watchScroll = false
console.log(`scroller[${debugname}] - reached top of the page`)
await loadBeforeWrapper()
}
if (!bottomReached && element.scrollHeight - element.scrollTop <= element.clientHeight + margin) {
watchScroll = false
console.log(`scroller[${debugname}] - reached bottom of the page`)
await loadAfterWrapper()
}
}
</script>
<div class="scroll-container" class:bottomaligned={bottomAligned} onscroll={handleScroll} bind:this={scrollContainer}>
{#if messages && messages.length > 0}
{#if topReached}
{@render channelStartSnippet(null, messages[0], null)}
{/if}
{#each messages as message, index (message._id)}
{@render renderMessageSnippet(lowestLoadedIndex + index, message, messages[index - 1])}
{/each}
{#if bottomReached}
{@render channelEndSnippet(null, null, null)}
{/if}
{/if}
</div>
<style>
.scroll-container {
height: 100%;
overflow-y: auto;
}
/* Needed for bottom aligment */
/* can't use justify-content: flex-end, because that would break scroll */
/* https://stackoverflow.com/a/37515194 */
.scroll-container.bottomaligned {
display: flex;
flex-flow: column nowrap;
padding-bottom: 32px;
}
/* align messages to the bottom if there are not enough messages to fill the container height */
:global(.scroll-container.bottomaligned > :first-child) {
margin-top: auto !important;
}
/* - */
.scroll-container::-webkit-scrollbar-track {
background-color: #2b2d31;
}
.scroll-container::-webkit-scrollbar-corner {
background-color: #646464;
}
.scroll-container::-webkit-resizer {
background-color: #666;
}
.scroll-container::-webkit-scrollbar-track-piece {
background-color:#313338;
}
.scroll-container::-webkit-scrollbar {
height: 3px;
width: 14px;
}
.scroll-container::-webkit-scrollbar-thumb {
height: 50px;
background-color: #1a1b1e;
width: 5px;
border-radius: 10px;
/*left+right scrollbar padding magix*/
background-clip: padding-box;
border: 3px solid rgba(0, 0, 0, 0);
}
</style>

View file

@ -13,11 +13,15 @@
selectedMessageId: string | null
bottomAligned: boolean
debugname: string
loadBefore: () => Promise<void>
loadAfter: () => Promise<void>
renderMessageSnippet: (index: number, message: Message, previousMessage: Message) => void
channelStartSnippet: (index: number, message: Message, previousMessage: Message) => void
channelEndSnippet: (index: number, message: Message, previousMessage: Message) => void
}
let { ids, guildId, selectedMessageId = null, renderMessageSnippet, bottomAligned, debugname=""}: MyProps = $props();
let { ids, guildId, selectedMessageId = null, renderMessageSnippet, channelStartSnippet, channelEndSnippet, bottomAligned, debugname="", loadBefore, loadAfter}: MyProps = $props();
let maxMessages = 120
let maxMessages = 12000
let loadIncrement = 30
let startLoadingPixels = 500
let scrollContainer: HTMLDivElement
@ -26,10 +30,14 @@
let lowestLoadedIndex: number | null = $state(null)
let highestLoadedIndex: number | null = $state(null)
let loadedIds: string[] = $state([])
let topReached = $derived(loadedIds.includes("first"))
let bottomReached = $derived(loadedIds.includes("last"))
let watchScroll: boolean = $state(false)
let topLoaded = $derived(lowestLoadedIndex === 0)
// let bottomLoaded = $derived(highestLoadedIndex === ids.length - 1) // not used
console.warn(`scroller[${debugname}] - RELOADED`, ids)
let isInitialLoad = true
async function refetchMessages(loadedIds: string[]) {
messages = await messsageIdsToMessages(guildId, loadedIds)
@ -58,102 +66,116 @@
centerIndex = Math.round(ids.length - 1)
}
highestLoadedIndex = centerIndex
lowestLoadedIndex = centerIndex
loadedIds = [ids[centerIndex]]
for (let i = 1; i < maxMessages; i++) { // TODO: rewrite using slice
if (centerIndex + i < ids.length) {
highestLoadedIndex = centerIndex + i
loadedIds = [...loadedIds, ids[highestLoadedIndex]]
}
if (centerIndex - i >= 0) {
lowestLoadedIndex = centerIndex - i
loadedIds = [ids[lowestLoadedIndex], ...loadedIds, ]
}
}
// highestLoadedIndex = centerIndex
// lowestLoadedIndex = centerIndex
// loadedIds = [ids[centerIndex]]
// for (let i = 1; i < maxMessages; i++) { // TODO: rewrite using slice
// if (centerIndex + i < ids.length) {
// highestLoadedIndex = centerIndex + i
// loadedIds = [...loadedIds, ids[highestLoadedIndex]]
// }
// if (centerIndex - i >= 0) {
// lowestLoadedIndex = centerIndex - i
// loadedIds = [ids[lowestLoadedIndex], ...loadedIds, ]
// }
// }
// loadedIds = [...ids]
loadedIds = ids
await refetchMessages(loadedIds)
setTimeout(async() => {
if (!scrollContainer) { // did we destroy the component before the timeout happened?
return
}
// scroll to selected message
if (selectedMessageId) {
let failedToScroll = true
// find selected message index
if (loadedIds.includes(selectedMessageId)) {
let selectedMessageElement = scrollContainer.querySelector(`[data-messageid="${selectedMessageId}"]`)
if (selectedMessageElement) {
selectedMessageElement.scrollIntoView({ block: "start", behavior: "instant" })
console.log(`scroller[${debugname}] - selected message scrolled into view`)
failedToScroll = false
}
if (isInitialLoad) {
isInitialLoad = false
setTimeout(async() => {
if (!scrollContainer) { // did we destroy the component before the timeout happened?
return
}
if (failedToScroll) {
if (ids.includes(selectedMessageId)) {
console.warn(`scroller[${debugname}] - selected message not found in loaded messages BUT found in all messages`)
// scroll to selected message
if (selectedMessageId) {
let failedToScroll = true
// find selected message index
if (loadedIds.includes(selectedMessageId)) {
let selectedMessageElement = scrollContainer.querySelector(`[data-messageid="${selectedMessageId}"]`)
if (selectedMessageElement) {
selectedMessageElement.scrollIntoView({ block: "start", behavior: "instant" })
console.log(`scroller[${debugname}] - selected message scrolled into view`)
failedToScroll = false
}
}
else {
console.warn(`scroller[${debugname}] - selected message not found in loaded messages and in all messages`)
if (failedToScroll) {
if (ids.includes(selectedMessageId)) {
console.warn(`scroller[${debugname}] - selected message not found in loaded messages BUT found in all messages`)
}
else {
console.warn(`scroller[${debugname}] - selected message not found in loaded messages and in all messages`)
}
}
}
}
else {
scrollContainer.scrollTop = scrollContainer.scrollHeight
console.log(`scroller[${debugname}] - no selected message to scroll to`)
}
watchScroll = true
}, 0)
else {
scrollContainer.scrollTop = scrollContainer.scrollHeight
console.log(`scroller[${debugname}] - no selected message to scroll to`)
}
watchScroll = true
}, 0)
}
// console.log(`scroller[${debugname}] - loadedIds`, loadedIds)
}
async function loadLowUnloadHigh() {
for (let i = 1; i < loadIncrement; i++) { // TODO: rewrite using slice
if (lowestLoadedIndex - 1 >= 0) {
// add lowest
lowestLoadedIndex = lowestLoadedIndex - 1
loadedIds = [ids[lowestLoadedIndex], ...loadedIds]
async function loadBeforeWrapper() {
console.log(`scroller[${debugname}] - loadBeforeWrapper before`, loadedIds.length)
console.log(`scroller[${debugname}] - loadBefore`, loadBefore)
await loadBefore()
// for (let i = 1; i < loadIncrement; i++) { // TODO: rewrite using slice
// if (!topReached) {
// // // add lowest
// // lowestLoadedIndex = lowestLoadedIndex - 1
// // loadedIds = [ids[lowestLoadedIndex], ...loadedIds]
// remove highest
if (loadedIds.length > maxMessages) {
highestLoadedIndex = highestLoadedIndex - 1
loadedIds = loadedIds.slice(0, -1)
}
}
else {
console.log(`scroller[${debugname}] - top of the page - No more messages to load.`)
break
}
}
console.log(`scroller[${debugname}] - loadLowUnloadHigh`)
// // // remove highest
// // if (loadedIds.length > maxMessages) {
// // highestLoadedIndex = highestLoadedIndex - 1
// // loadedIds = loadedIds.slice(0, -1)
// // }
// }
// else {
// console.log(`scroller[${debugname}] - top of the page - No more messages to load.`)
// break
// }
// }
await refetchMessages(loadedIds)
// await refetchMessages(loadedIds)
console.log(`scroller[${debugname}] - loadBeforeWrapper after`, loadedIds.length)
watchScroll = true
}
async function loadHighUnloadLow() {
for (let i = 1; i < loadIncrement; i++) { // TODO: rewrite using slice
if (highestLoadedIndex + 1 < ids.length) {
// add highest
highestLoadedIndex = highestLoadedIndex + 1
loadedIds = [...loadedIds, ids[highestLoadedIndex]]
async function loadAfterWrapper() {
console.log(`scroller[${debugname}] - loadAfterWrapper before`, loadedIds.length)
console.log(`scroller[${debugname}] - loadAfter`, loadAfter)
await loadAfter()
// for (let i = 1; i < loadIncrement; i++) { // TODO: rewrite using slice
// if (!bottomReached) {
// // // add highest
// // highestLoadedIndex = highestLoadedIndex + 1
// // loadedIds = [...loadedIds, ids[highestLoadedIndex]]
// remove lowest
if (loadedIds.length > maxMessages) {
lowestLoadedIndex = lowestLoadedIndex + 1
loadedIds = loadedIds.slice(1)
}
}
else {
console.log(`scroller[${debugname}] - bottom of the page - No more messages to load.`)
break
}
}
console.log(`scroller[${debugname}] - loadHighUnloadLow`)
// // // remove lowest
// // if (loadedIds.length > maxMessages) {
// // lowestLoadedIndex = lowestLoadedIndex + 1
// // loadedIds = loadedIds.slice(1)
// // }
// }
// else {
// console.log(`scroller[${debugname}] - bottom of the page - No more messages to load.`)
// break
// }
// }
console.log(`scroller[${debugname}] - loadAfterWrapper after`, loadedIds.length)
await refetchMessages(loadedIds)
// await refetchMessages(loadedIds)
watchScroll = true
}
@ -179,26 +201,36 @@
if (messages.length === 0)
return
// console.log(`scroller[${debugname}] - handleScroll`, JSON.parse(JSON.stringify(loadedIds)))
// console.log(`scroller[${debugname}] - handleScroll`, loadedIds.length)
const element = event.target
let margin = startLoadingPixels
if (element.scrollTop < margin) {
// console.log(`scroller[${debugname}] - scrollTop ${element.scrollTop}`)
if (!topReached && element.scrollTop < margin) {
watchScroll = false
console.log(`scroller[${debugname}] - reached top of the page`)
loadLowUnloadHigh()
await loadBeforeWrapper()
}
if (element.scrollHeight - element.scrollTop <= element.clientHeight + margin) {
if (!bottomReached && element.scrollHeight - element.scrollTop <= element.clientHeight + margin) {
watchScroll = false
console.log(`scroller[${debugname}] - reached bottom of the page`)
loadHighUnloadLow()
await loadAfterWrapper()
}
}
</script>
<div class="scroll-container" class:bottomaligned={bottomAligned} onscroll={handleScroll} bind:this={scrollContainer}>
{#if messages}
{#if messages && messages.length > 0}
{#if topReached}
{@render channelStartSnippet(null, messages[0], null)}
{/if}
{#each messages as message, index (message._id)}
{@render renderMessageSnippet(lowestLoadedIndex + index, message, messages[index - 1])}
{/each}
{#if bottomReached}
{@render channelEndSnippet(null, null, null)}
{/if}
{/if}
</div>

View file

@ -0,0 +1,238 @@
<script lang="ts">
import { onMount, tick, type Snippet } from "svelte";
import { messsageIdsToMessages } from "../js/messages";
interface MyProps {
fetchMessageIds: (direction: "before" | "after" | "around" , messageId: string, limit: number) => Promise<string[]>
guildId: string
snippetMessage: Snippet
}
let { fetchMessageIds, guildId, snippetMessage}: MyProps = $props();
let SHOWDEBUG = true
// message id can be 24 length strings or be "first" or "last"
let ids = $state<string[]>([])
let renderedIds = $state<string[]>([])
let messages = $state<any[]>([])
let topReached = $derived(renderedIds.includes("first"))
let bottomReached = $derived(renderedIds.includes("last"))
let earliestRenderedId = $state<string | null>(null)
// const renderedCount = 50
let scrollContainerHeight = $state(1000)
let scrollableHeight = $state(2000)
let maxRenderedCount = $derived(Math.max(100, Math.ceil(scrollContainerHeight / 10)))
let marginPixels = $derived((scrollableHeight - scrollContainerHeight) / 8)
async function fetchIdsBefore(messageId: string) {
const newMessageIds = await fetchMessageIds("before", messageId, maxRenderedCount)
return [...newMessageIds, ...ids]
}
async function fetchIdsAfter(messageId: string) {
const newMessageIds = await fetchMessageIds("after", messageId, maxRenderedCount)
return [...ids, ...newMessageIds]
}
async function fetchIdsAround(messageId: string) {
const newMessageIds = await fetchMessageIds("around", messageId, maxRenderedCount)
return newMessageIds
}
function calculateRenderedIdsAround(messageId: string) {
if (!ids.includes(messageId)) {
console.warn(`messageId ${messageId} not found in ids`)
return
}
const countToRender = Math.min(maxRenderedCount, ids.length)
let suggestedToRender = [messageId]
let renderedCount = 1
for (let i = 1; i < countToRender; i++) {
const before = ids[ids.indexOf(messageId) - i]
const after = ids[ids.indexOf(messageId) + i]
if (before) {
suggestedToRender.unshift(before)
renderedCount++
}
if (after) {
suggestedToRender.push(after)
renderedCount++
}
if (renderedCount >= countToRender) {
break
}
}
return suggestedToRender
}
let watchScroll = $state(true)
async function handleScroll(event) {
scrollableHeight = event.target.scrollHeight
if (ids.length === 0) {
return
}
if (!watchScroll) {
return
}
const element = event.target
const scrollTop = element.scrollTop
const scrollBottom = element.scrollHeight - element.clientHeight - scrollTop
if (!topReached && scrollTop < marginPixels) {
watchScroll = false
// save position of first rendered message
const id = renderedIds[0]
const firstRenderedMessage = element.querySelector(`[data-messageid="${id}"]`)
// let position
// if (firstRenderedMessage) {
// position = firstRenderedMessage.getBoundingClientRect()
// }
// else {
// console.warn(`firstRenderedMessage ${id} not found`)
// }
if (ids[0] === renderedIds[0]) { // if not cached
const newIds = await fetchIdsBefore(ids[0])
ids = newIds
}
renderedIds = calculateRenderedIdsAround(id)
messages = await messsageIdsToMessages(guildId, renderedIds)
// renderedIds = newIds
await tick(); // wait for render
// restore scroll position
// if (position) {
// const newFirstRenderedMessage = element.querySelector(`[data-messageid="${id}"]`)
// if (newFirstRenderedMessage) {
// const newPosition = newFirstRenderedMessage.getBoundingClientRect()
// element.scrollTop = newPosition.top - position.top + scrollTop
// }
// else {
// console.warn(`newFirstRenderedMessage ${id} not found`)
// }
// }
watchScroll = true
}
else if (!bottomReached && scrollBottom < marginPixels) {
watchScroll = false
const id = renderedIds[renderedIds.length - 1]
if (ids[ids.length - 1] === renderedIds[renderedIds.length - 1]) { // if not cached
const newIds = await fetchIdsAfter(ids[ids.length - 1])
ids = newIds
}
renderedIds = calculateRenderedIdsAround(id)
messages = await messsageIdsToMessages(guildId, renderedIds)
// renderedIds = newIds
await tick(); // wait for render
watchScroll = true
}
}
let scrollContainer: HTMLDivElement
onMount(async () => {
const newMessageIds = await fetchMessageIds("around", "last", maxRenderedCount)
ids = newMessageIds
renderedIds = calculateRenderedIdsAround("last")
messages = await messsageIdsToMessages(guildId, renderedIds)
await tick();
// const tout = setTimeout(async() => {
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight
console.log("scrolling to bottom")
}
else {
console.warn("scrollContainer not found")
}
// }, 0)
// const inter1 = setInterval(() => {
// if (scrollContainer) {
// const scrollTop = scrollContainer.scrollTop
// if (!topReached && scrollTop < 10) {
// scrollContainer.scrollTop = 150
// console.log("fixing scroll position")
// return
// }
// }
// }, 1000)
// return () => {
// clearInterval(inter1)
// }
// return () => {
// clearTimeout(tout)
// }
})
</script>
<div class="wrapper">
{#if SHOWDEBUG}
<small class="debug-container">rendered {renderedIds.length} / {ids.length}, topReached {topReached} ({renderedIds[0]}), bottomReached {bottomReached} ({renderedIds[renderedIds.length - 1]}), container height {scrollContainerHeight}px, maxRenderedCount {maxRenderedCount}, scrollableHeight {scrollableHeight}px, marginPixels {marginPixels}px</small>
{/if}
<div class="scroll-container" onscroll={handleScroll} bind:this={scrollContainer} bind:clientHeight={scrollContainerHeight}>
{#each messages as message (message._id)}
<div class="message" data-messageid={message._id}>
<!-- <div>{message._id}</div> -->
{@render snippetMessage(message, message)}
</div>
{/each}
</div>
</div>
<style>
.wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
.message {
min-height: 30px;
}
.scroll-container {
flex: 1;
overflow-y: auto;
}
.scroll-container::-webkit-scrollbar-track {
background-color: #2b2d31;
}
.scroll-container::-webkit-scrollbar-corner {
background-color: #646464;
}
.scroll-container::-webkit-resizer {
background-color: #666;
}
.scroll-container::-webkit-scrollbar-track-piece {
background-color:#313338;
}
.scroll-container::-webkit-scrollbar {
height: 3px;
width: 14px;
}
.scroll-container::-webkit-scrollbar-thumb {
height: 50px;
background-color: #1a1b1e;
width: 5px;
border-radius: 10px;
/*left+right scrollbar padding magix*/
background-clip: padding-box;
border: 3px solid rgba(0, 0, 0, 0);
}
</style>

View file

@ -4,6 +4,7 @@
import { getLayoutState } from "../js/stores/layoutState.svelte";
import DateSeparator from "./DateSeparator.svelte";
import InfiniteScroll from "./InfiniteScroll.svelte";
import InfiniteScroll2 from "./InfiniteScroll2.svelte";
import Pinned from "./Pinned.svelte";
import Icon from "./icons/Icon.svelte";
import ChannelIcon from "./menuchannels/ChannelIcon.svelte";
@ -17,12 +18,27 @@
const guildState = getGuildState()
const layoutState = getLayoutState()
let apiGuildId = $derived(guildState.guildId ? guildState.guildId : "000000000000000000000000")
let apiChannelId = $derived(guildState.threadId)
export async function fetchMessageIds(direction: "before" | "after" | "around" | "first" | "last", messageId: string | null = null, limit: number) {
try {
let response = await fetch(`/api/message-ids-paginated?guild_id=${encodeURIComponent(apiGuildId)}&channel_id=${encodeURIComponent(apiChannelId)}&direction=${direction}&message_id=${encodeURIComponent(messageId)}&limit=${limit}`)
let messageIds = await response.json()
return messageIds
}
catch (e) {
console.error("api - Failed to fetch message ids", e)
return []
}
}
</script>
{#snippet renderMessageSnippet(index, message, previousMessage)}
{#if index === 0}
<!-- {#if index === 0}
<ChannelStart channelName={message.channelName} isThread={true} messageAuthor={message.author} />
{/if}
{/if} -->
{#if isDateDifferent(previousMessage, message)}
<DateSeparator messageId={message._id} />
@ -33,6 +49,22 @@
</div>
{/snippet}
{#snippet renderMessageSnippet2(message, previousMessage)}
<div data-messageid={message._id}>
{#if message._id === "first"}
<!-- <ChannelStart channelName={message.channelName} isThread={true} messageAuthor={message.author} /> -->
<div>thread start</div>
{:else if message._id === "last"}
<div>thread end</div>
{:else}
{#if isDateDifferent(previousMessage, message)}
<DateSeparator messageId={message._id} />
{/if}
<Message message={message} previousMessage={previousMessage} />
{/if}
</div>
{/snippet}
<div class="thread-wrapper">
<div class="header-main">
@ -62,7 +94,20 @@
<div class="thread">
<!-- TODO: support change of threadMessageId without rerender -->
{#key guildState.threadMessageId}
<InfiniteScroll debugname="thread" ids={guildState.threadMessagesIds} guildId={guildState.guildId} selectedMessageId={guildState.threadMessageId} renderMessageSnippet={renderMessageSnippet} bottomAligned={true} />
<!-- <InfiniteScroll
debugname="thread"
ids={guildState.threadMessagesIds}
guildId={guildState.guildId}
selectedMessageId={guildState.threadMessageId}
renderMessageSnippet={renderMessageSnippet}
channelStartSnippet={channelStartSnippet}
channelEndSnippet={channelEndSnippet}
bottomAligned={true} /> -->
<InfiniteScroll2
fetchMessageIds={fetchMessageIds}
guildId={apiGuildId}
snippetMessage={renderMessageSnippet2}
/>
{/key}
</div>
</div>

View file

@ -18,7 +18,9 @@
async function toggle() {
isOpen = !isOpen
if (isOpen) {
await guildState.changeChannelId(channel._id)
if (guildState.channelId !== channel._id) {
await guildState.changeChannelId(channel._id, "last")
}
await guildState.pushState()
}
}

View file

@ -35,8 +35,12 @@
async function changeThread(guildId: string, channelId: string, threadId: string) {
await guildState.changeGuildId(guildId)
await guildState.changeChannelId(channelId)
await guildState.changeThreadId(threadId)
if (guildState.channelId !== channelId) {
await guildState.changeChannelId(channelId, "last")
}
if (guildState.threadId !== threadId) {
await guildState.changeThreadId(threadId, "last")
}
await guildState.pushState()
}

View file

@ -13,7 +13,7 @@
</script>
{#if isThread}
<div class="wrapper">
<div class="wrapper" data-messageid="first">
<div class="thread-icon">
<Icon name="channeltype/thread" width={30} />
</div>
@ -21,7 +21,7 @@
<div class="subtitle">Started by <span class="subtitle-person"><MessageAuthorName author={messageAuthor} on:click={() => viewUserState.setUser(messageAuthor)} /></span></div>
</div>
{:else}
<div class="wrapper">
<div class="wrapper" data-messageid="first">
<div class="channel-icon">
<Icon name="channeltype/channel" width={42} />
</div>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { findChannel, findThread, getGuildState, isChannel } from '../../js/stores/guildState.svelte';
import InfiniteScroll from '../InfiniteScroll.svelte';
import InfiniteScroll from '../InfiniteScroll-old.svelte';
import Icon from '../icons/Icon.svelte';
import ChannelIcon from '../menuchannels/ChannelIcon.svelte';
import Message from '../message/Message.svelte';
@ -24,10 +24,10 @@
{#if threadObj}
{@const parentChannelObj = findChannel(threadObj.categoryId)}
<div class="channelthread-name-wrapper">
<button class="thread-name" onclick={()=>guildState.changeThreadId(threadObj._id)}>
<button class="thread-name" onclick={()=>guildState.changeThreadId(threadObj._id, null)}>
<ChannelIcon channel={threadObj} width={16} />{threadObj.name}
</button>
<button class="channel-name-small" onclick={()=>guildState.changeChannelId(parentChannelObj._id)}>
<button class="channel-name-small" onclick={()=>guildState.changeChannelId(parentChannelObj._id, null)}>
<ChannelIcon channel={parentChannelObj} width={12} />{parentChannelObj.name}
</button>
</div>
@ -35,9 +35,9 @@
<div class="channelthread-name-wrapper">
<button class="channelthread-name" onclick={()=>{
if (isChannel(channelObj._id)) {
guildState.changeChannelId(channelObj._id)
guildState.changeChannelId(channelObj._id, null)
} else {
guildState.changeThreadId(channelObj._id)
guildState.changeThreadId(channelObj._id, null)
}
}}>
<ChannelIcon channel={channelObj} width={16} />{channelObj.name}
@ -75,7 +75,14 @@
<div class="header-txt">{addCommas(searchState.searchResultsIds.length)} Results</div>
</div>
{#key searchState.submittedSearchPrompt}
<InfiniteScroll debugname="search" ids={searchState.searchResultsIds} guildId={guildState.guildId} selectedMessageId={searchState.searchResultsIds[0]} renderMessageSnippet={renderMessageSnippet} bottomAligned={false} />
<InfiniteScroll
debugname="search"
ids={searchState.searchResultsIds}
guildId={guildState.guildId}
selectedMessageId={searchState.searchResultsIds[0]}
renderMessageSnippet={renderMessageSnippet}
bottomAligned={false}
/>
{/key}
</div>
{/if}