Implement source pages

This commit is contained in:
thepaperpilot 2024-05-07 00:02:09 -05:00
parent 8249bcc18c
commit caecd859a1
4 changed files with 164 additions and 65 deletions

View file

@ -27,18 +27,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue'; import { toRef } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { Category } from '../../state';
import { ThreadRef, sources, Category } from '../../state'; import { setupSelectedThread } from '../../utils';
import CategoryHeader from "../CategoryHeader.vue"; import CategoryHeader from "../CategoryHeader.vue";
import ItemGroup from "../ItemGroup.vue"; import ItemGroup from "../ItemGroup.vue";
import Modal from '../Modal.vue'; import Modal from '../Modal.vue';
import PanelTitle from '../PanelTitle.vue'; import PanelTitle from '../PanelTitle.vue';
import SnoozeModal from '../SnoozeModal.vue'; import SnoozeModal from '../SnoozeModal.vue';
import Thread from '../Thread.vue'; import Thread from '../Thread.vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const props = defineProps<{ const props = defineProps<{
title: string; title: string;
@ -47,63 +45,6 @@ const props = defineProps<{
showSnoozed?: boolean; showSnoozed?: boolean;
}>(); }>();
const showThread = ref(false); const { showThread, selectedItem, snoozingItem, snoozing, threadTitle, selectItem, snoozeItem, close, stopSnoozing } =
const selectedItem = ref<ThreadRef | undefined>(); setupSelectedThread(toRef(props, "baseUrl"), useRoute(), useRouter());
const snoozingItem = ref<ThreadRef | undefined>();
const snoozing = ref(false);
watch([() => route.params.source, () => route.params.sourceItem, () => route.params.thread], ([source, sourceItem, thread]) => {
const sourceInt = parseInt(Array.isArray(source) ? source[0] : source);
const sourceItemInt = parseInt(Array.isArray(sourceItem) ? sourceItem[0] : sourceItem);
const threadInt = parseInt(Array.isArray(thread) ? thread[0] : thread);
if (!isNaN(sourceInt) && !isNaN(sourceItemInt) && !isNaN(threadInt)) {
selectedItem.value = { source: sourceInt, sourceItem: sourceItemInt, thread: threadInt };
showThread.value = true;
} else {
// Intentionally leave selectedItem so animations look right
showThread.value = false;
}
}, { immediate: true });
const threadTitle = computed(() => {
const selected = selectedItem.value;
if (selected == null) {
return "";
}
return sources.value[selected.source].items[selected.sourceItem].threads[selected.thread].title;
});
function selectItem(source: number, sourceItem: number, thread: number) {
const p = route.params;
const sourceInt = parseInt(Array.isArray(p.source) ? p.source[0] : p.source);
const sourceItemInt = parseInt(Array.isArray(p.sourceItem) ? p.sourceItem[0] : p.sourceItem);
const threadInt = parseInt(Array.isArray(p.thread) ? p.thread[0] : p.thread);
if (source === sourceInt && sourceItem === sourceItemInt && thread === threadInt) {
close();
return false;
}
router.push(`${props.baseUrl}/${source}/${sourceItem}/${thread}`);
return true;
}
function snoozeItem(source: number, sourceItem: number, thread: number) {
snoozingItem.value = { source, sourceItem, thread };
snoozing.value = true;
}
function close() {
console.log("closing thread")
router.push(props.baseUrl);
snoozing.value = false;
}
function stopSnoozing() {
if (snoozingItem.value?.source === selectedItem.value?.source &&
snoozingItem.value?.sourceItem === selectedItem.value?.sourceItem &&
(snoozingItem.value?.thread === -1 || snoozingItem.value?.thread === selectedItem.value?.thread)
) {
close();
} else {
snoozing.value = false;
}
}
</script> </script>

View file

@ -0,0 +1,84 @@
<template>
<div class="flex h-full">
<div class="grow basis-6/12 p-4 overflow-x-hidden relative">
<div class="text-3xl mb-4">{{ source.name }}</div>
<div v-if="Object.keys(source.items).length === 0" class="absolute center text-9xl"></div>
<Item v-for="item in items"
:item="item"
:selected-thread="showThread ? selectedItem : undefined"
@select="thread => selectItem(item.sourceItem, thread)"
@snoozeItem="thread => snoozeItem(sourceId, item.sourceItem, thread)"
@deselect="close" />
</div>
<div class="hidden xl:flex bg-slate-200 ml-0 h-screen flex-col overflow-hidden" :class="showThread ? 'p-4 basis-6/12' : 'basis-0'">
<PanelTitle :title="threadTitle" />
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
</div>
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
<template v-slot:header><div class="text-3xl">{{ threadTitle }}</div></template>
<template v-slot:body><Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" /></template>
</Modal>
<SnoozeModal :snoozing="snoozing" :threadRef="snoozingItem" @close="stopSnoozing" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ItemThread, MessageEvent, sources } from '../../state';
import { deArray, setupSelectedThread } from '../../utils';
import Item from '../Item.vue';
import Modal from '../Modal.vue';
import PanelTitle from '../PanelTitle.vue';
import SnoozeModal from '../SnoozeModal.vue';
import Thread from '../Thread.vue';
const route = useRoute();
const router = useRouter();
const baseUrl = computed(() => `/source/${route.params.source}`);
const sourceId = computed(() => parseInt(deArray(route.params.source)));
const source = computed(() => sources.value[sourceId.value]);
const items = computed(() => Object.keys(source.value.items).map(item => {
const sourceItem = parseInt(item);
const sourceThreads = source.value.items[sourceItem].threads;
let updatedAt = 0;
const itemThreads = Object.keys(sourceThreads).reduce((acc, curr) => {
const thread = sourceThreads[parseInt(curr)];
const lastMessage = thread.timeline.findLast(msg => msg.type === 'message') as MessageEvent | undefined;
const threadUpdated = thread.timeline[thread.timeline.length - 1]?.time ?? 0;
updatedAt = Math.max(updatedAt, threadUpdated);
acc[parseInt(curr)] = {
count: 0,
preview: lastMessage?.message ?? "",
updatedAt: threadUpdated,
contact: lastMessage?.contact
};
return acc;
}, {} as Record<number, ItemThread>);
return {
source: sourceId.value,
sourceItem,
updatedAt,
threads: itemThreads
}
}));
function selectItem(sourceItem: number, thread: number) {
const p = route.params;
const sourceItemInt = parseInt(Array.isArray(p.sourceItem) ? p.sourceItem[0] : p.sourceItem);
const threadInt = parseInt(Array.isArray(p.thread) ? p.thread[0] : p.thread);
if (sourceItem === sourceItemInt && thread === threadInt) {
close();
return false;
}
router.push(`${baseUrl.value}/${sourceItem}/${thread}`);
return true;
}
const { showThread, selectedItem, snoozingItem, snoozing, threadTitle, snoozeItem, close, stopSnoozing } =
setupSelectedThread(baseUrl, route, router);
</script>

View file

@ -6,12 +6,14 @@ import App from "./App.vue";
import Todo from "./components/pages/Todo.vue"; import Todo from "./components/pages/Todo.vue";
import Snoozed from "./components/pages/Snoozed.vue"; import Snoozed from "./components/pages/Snoozed.vue";
import LowPriority from "./components/pages/LowPriority.vue"; import LowPriority from "./components/pages/LowPriority.vue";
import Source from "./components/pages/Source.vue";
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ path: '/', component: Todo, name: 'home' }, { path: '/', component: Todo, name: 'home' },
{ path: '/todo/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Todo, name: 'todo' }, { path: '/todo/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Todo, name: 'todo' },
{ path: '/snoozed/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Snoozed, name: 'snoozed' }, { path: '/snoozed/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Snoozed, name: 'snoozed' },
{ path: '/low/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: LowPriority, name: 'low' }, { path: '/low/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: LowPriority, name: 'low' },
{ path: '/source/:source(\\d+)/:sourceItem(\\d+)?/:thread(\\d+)?', component: Source, name: 'source' },
]; ];
const router = createRouter({ const router = createRouter({

View file

@ -1,3 +1,7 @@
import { Ref, computed, ref, watch } from "vue";
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router";
import { ThreadRef, sources } from "./state";
// Modified from https://www.basedash.com/blog/how-to-merge-two-sorted-lists-in-javascript // Modified from https://www.basedash.com/blog/how-to-merge-two-sorted-lists-in-javascript
export function mergeSortedLists<T>(arr1: Array<T>, arr2: Array<T>): Array<T> { export function mergeSortedLists<T>(arr1: Array<T>, arr2: Array<T>): Array<T> {
const merged: Array<T> = []; const merged: Array<T> = [];
@ -15,3 +19,71 @@ export function mergeSortedLists<T>(arr1: Array<T>, arr2: Array<T>): Array<T> {
// Append any remaining elements from arr1 or arr2 // Append any remaining elements from arr1 or arr2
return [...merged, ...arr1.slice(i), ...arr2.slice(j)]; return [...merged, ...arr1.slice(i), ...arr2.slice(j)];
} }
export function deArray<T>(value: T | T[]): T {
return Array.isArray(value) ? value[0] : value;
}
export function setupSelectedThread(baseUrl: Ref<string>, route: RouteLocationNormalizedLoaded, router: Router) {
const showThread = ref(false);
const selectedItem = ref<ThreadRef | undefined>();
const snoozingItem = ref<ThreadRef | undefined>();
const snoozing = ref(false);
watch([() => route.params.source, () => route.params.sourceItem, () => route.params.thread], ([source, sourceItem, thread]) => {
const sourceInt = parseInt(Array.isArray(source) ? source[0] : source);
const sourceItemInt = parseInt(Array.isArray(sourceItem) ? sourceItem[0] : sourceItem);
const threadInt = parseInt(Array.isArray(thread) ? thread[0] : thread);
if (!isNaN(sourceInt) && !isNaN(sourceItemInt) && !isNaN(threadInt)) {
selectedItem.value = { source: sourceInt, sourceItem: sourceItemInt, thread: threadInt };
showThread.value = true;
} else {
// Intentionally leave selectedItem so animations look right
showThread.value = false;
}
}, { immediate: true });
const threadTitle = computed(() => {
const selected = selectedItem.value;
if (selected == null) {
return "";
}
return sources.value[selected.source].items[selected.sourceItem].threads[selected.thread].title;
});
function selectItem(source: number, sourceItem: number, thread: number) {
const p = route.params;
const sourceInt = parseInt(Array.isArray(p.source) ? p.source[0] : p.source);
const sourceItemInt = parseInt(Array.isArray(p.sourceItem) ? p.sourceItem[0] : p.sourceItem);
const threadInt = parseInt(Array.isArray(p.thread) ? p.thread[0] : p.thread);
if (source === sourceInt && sourceItem === sourceItemInt && thread === threadInt) {
close();
return false;
}
router.push(`${baseUrl.value}/${source}/${sourceItem}/${thread}`);
return true;
}
function snoozeItem(source: number, sourceItem: number, thread: number) {
snoozingItem.value = { source, sourceItem, thread };
snoozing.value = true;
}
function close() {
console.log("closing thread")
router.push(baseUrl.value);
snoozing.value = false;
}
function stopSnoozing() {
if (snoozingItem.value?.source === selectedItem.value?.source &&
snoozingItem.value?.sourceItem === selectedItem.value?.sourceItem &&
(snoozingItem.value?.thread === -1 || snoozingItem.value?.thread === selectedItem.value?.thread)
) {
close();
} else {
snoozing.value = false;
}
}
return { showThread, selectedItem, snoozingItem, snoozing, threadTitle, selectItem, snoozeItem, close, stopSnoozing };
}