Implement marking threads as read
This commit is contained in:
parent
c4e6d7286a
commit
17806f539e
8 changed files with 185 additions and 22 deletions
|
@ -5,7 +5,8 @@
|
||||||
<Avatar v-if="getContact(0)" v-bind="getContact(0)!" class="mr-2" />
|
<Avatar v-if="getContact(0)" v-bind="getContact(0)!" class="mr-2" />
|
||||||
<span class="bg-gray-300 px-2 py-1 rounded-2xl block overflow-hidden text-nowrap text-ellipsis">{{ item.threads[0].preview }}</span>
|
<span class="bg-gray-300 px-2 py-1 rounded-2xl block overflow-hidden text-nowrap text-ellipsis">{{ item.threads[0].preview }}</span>
|
||||||
<span class="grow" />
|
<span class="grow" />
|
||||||
<span v-if="item.threads[0].count > 1" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-900 dark:text-blue-300">{{ item.threads[0].count }}</span>
|
<span v-if="item.threads[0].count > 0" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-900 dark:text-blue-300">{{ item.threads[0].count }}</span>
|
||||||
|
<span v-else><FontAwesomeIcon class="w-6 h-6 text-lime-600" :icon="faCheck" /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex -mb-2 text-gray-500 text-sm font-normal">
|
<div class="flex -mb-2 text-gray-500 text-sm font-normal">
|
||||||
<span class="grow">{{ sourceItem.title }}</span>
|
<span class="grow">{{ sourceItem.title }}</span>
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="text-3xl mb-4 shrink-0">{{ title }}</div>
|
<div class="w-full flex mb-4">
|
||||||
|
<div class="text-3xl shrink-0 grow">{{ title }}</div>
|
||||||
|
<button @click="emits('toggleRead')">
|
||||||
|
<FontAwesomeIcon class="w-6 h-6" :icon="isRead ? faEnvelopeOpen : faCheck" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { faCheck, faEnvelopeOpen } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
isRead: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
toggleRead: [];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
div {
|
div {
|
||||||
width: calc((100vw - 23rem) / 2);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
@ -118,8 +118,12 @@ function snoozeUntil(date: Date) {
|
||||||
updatedAt: threadUpdated,
|
updatedAt: threadUpdated,
|
||||||
contact: lastMessage?.contact
|
contact: lastMessage?.contact
|
||||||
};
|
};
|
||||||
item = { source: selected.source, sourceItem: selected.sourceItem, updatedAt: threadUpdated, threads: { [selected.thread]: thread } };
|
items.value.push({
|
||||||
items.value.push(item);
|
source: selected.source,
|
||||||
|
sourceItem: selected.sourceItem,
|
||||||
|
updatedAt: threadUpdated,
|
||||||
|
threads: { [selected.thread]: thread }
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selected.thread === -1) {
|
if (selected.thread === -1) {
|
||||||
|
|
|
@ -15,19 +15,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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'">
|
<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" />
|
<PanelTitle :title="threadTitle" :isRead="isRead" @toggleRead="toggleRead" />
|
||||||
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
|
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
|
||||||
</div>
|
</div>
|
||||||
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
|
<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:header>
|
||||||
<template v-slot:body><Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" /></template>
|
<PanelTitle :title="threadTitle" :isRead="isRead" @toggleRead="toggleRead" />
|
||||||
|
</template>
|
||||||
|
<template v-slot:body>
|
||||||
|
<Thread
|
||||||
|
:source="selectedItem?.source ?? 0"
|
||||||
|
:sourceItem="selectedItem?.sourceItem ?? 0"
|
||||||
|
:thread="selectedItem?.thread ?? 0"
|
||||||
|
:base-url="baseUrl" />
|
||||||
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
<SnoozeModal :snoozing="snoozing" :threadRef="snoozingItem" @close="stopSnoozing" />
|
<SnoozeModal :snoozing="snoozing" :threadRef="snoozingItem" @close="stopSnoozing" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { toRef } from 'vue';
|
import { toRef, watch } from 'vue';
|
||||||
import { Category } from '../../state';
|
import { Category } from '../../state';
|
||||||
import { setupSelectedThread } from '../../utils';
|
import { setupSelectedThread } from '../../utils';
|
||||||
import CategoryHeader from "../CategoryHeader.vue";
|
import CategoryHeader from "../CategoryHeader.vue";
|
||||||
|
@ -45,6 +53,32 @@ const props = defineProps<{
|
||||||
showSnoozed?: boolean;
|
showSnoozed?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { showThread, selectedItem, snoozingItem, snoozing, threadTitle, selectItem, snoozeItem, close, stopSnoozing } =
|
const {
|
||||||
setupSelectedThread(toRef(props, "baseUrl"), useRoute(), useRouter());
|
showThread,
|
||||||
|
selectedItem,
|
||||||
|
snoozingItem,
|
||||||
|
snoozing,
|
||||||
|
threadTitle,
|
||||||
|
selectItem,
|
||||||
|
snoozeItem,
|
||||||
|
close,
|
||||||
|
stopSnoozing,
|
||||||
|
isRead,
|
||||||
|
toggleRead
|
||||||
|
} = setupSelectedThread(toRef(props, "baseUrl"), useRoute(), useRouter());
|
||||||
|
|
||||||
|
watch(() => props.categories, categories => {
|
||||||
|
if (selectedItem.value != null) {
|
||||||
|
const { source, sourceItem, thread } = selectedItem.value;
|
||||||
|
if (!categories.some(cat => {
|
||||||
|
const items = props.showSnoozed ? cat.snoozedItems : cat.activeItems;
|
||||||
|
return items.some(item =>
|
||||||
|
item.source === source &&
|
||||||
|
item.sourceItem === sourceItem &&
|
||||||
|
Object.keys(item.threads).includes(thread.toString()));
|
||||||
|
})) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
@deselect="close" />
|
@deselect="close" />
|
||||||
</div>
|
</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'">
|
<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" />
|
<PanelTitle :title="threadTitle" :isRead="isRead" @toggleRead="toggleRead" />
|
||||||
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
|
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
|
||||||
</div>
|
</div>
|
||||||
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
|
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ItemThread, MessageEvent, sources, items as stateItems } from '../../state';
|
import { ItemThread, MessageEvent, sources, items as stateItems } from '../../state';
|
||||||
import { deArray, setupSelectedThread } from '../../utils';
|
import { deArray, setupSelectedThread } from '../../utils';
|
||||||
|
@ -86,6 +86,28 @@ function selectItem(sourceItem: number, thread: number) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { showThread, selectedItem, snoozingItem, snoozing, threadTitle, snoozeItem, close, stopSnoozing } =
|
const {
|
||||||
setupSelectedThread(baseUrl, route, router);
|
showThread,
|
||||||
|
selectedItem,
|
||||||
|
snoozingItem,
|
||||||
|
snoozing,
|
||||||
|
threadTitle,
|
||||||
|
snoozeItem,
|
||||||
|
close,
|
||||||
|
stopSnoozing,
|
||||||
|
isRead,
|
||||||
|
toggleRead
|
||||||
|
} = setupSelectedThread(baseUrl, route, router);
|
||||||
|
|
||||||
|
watch(items, items => {
|
||||||
|
if (selectedItem.value != null) {
|
||||||
|
const { sourceItem, thread } = selectedItem.value;
|
||||||
|
if (!items.some(item =>
|
||||||
|
item.source === sourceId.value &&
|
||||||
|
item.sourceItem === sourceItem &&
|
||||||
|
Object.keys(item.threads).includes(thread.toString()))) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
@deselect="close" />
|
@deselect="close" />
|
||||||
</div>
|
</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'">
|
<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" />
|
<PanelTitle :title="threadTitle" :isRead="isRead" @toggleRead="toggleRead" />
|
||||||
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
|
<Thread :source="selectedItem?.source ?? 0" :sourceItem="selectedItem?.sourceItem ?? 0" :thread="selectedItem?.thread ?? 0" :base-url="baseUrl" />
|
||||||
</div>
|
</div>
|
||||||
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
|
<Modal :model-value="showThread" class="block xl:hidden" @update:model-value="close">
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { setupSelectedThread } from '../../utils';
|
import { setupSelectedThread } from '../../utils';
|
||||||
import Item from '../Item.vue';
|
import Item from '../Item.vue';
|
||||||
|
@ -35,6 +35,29 @@ import { unsortedItems } from '../../state';
|
||||||
|
|
||||||
const baseUrl = ref("/unsorted");
|
const baseUrl = ref("/unsorted");
|
||||||
|
|
||||||
const { showThread, selectedItem, snoozingItem, snoozing, threadTitle, selectItem, snoozeItem, close, stopSnoozing } =
|
const {
|
||||||
setupSelectedThread(baseUrl, useRoute(), useRouter());
|
showThread,
|
||||||
|
selectedItem,
|
||||||
|
snoozingItem,
|
||||||
|
snoozing,
|
||||||
|
threadTitle,
|
||||||
|
selectItem,
|
||||||
|
snoozeItem,
|
||||||
|
close,
|
||||||
|
stopSnoozing,
|
||||||
|
isRead,
|
||||||
|
toggleRead
|
||||||
|
} = setupSelectedThread(baseUrl, useRoute(), useRouter());
|
||||||
|
|
||||||
|
watch(unsortedItems, items => {
|
||||||
|
if (selectedItem.value != null) {
|
||||||
|
const { source, sourceItem, thread } = selectedItem.value;
|
||||||
|
if (!items.some(item =>
|
||||||
|
item.source === source &&
|
||||||
|
item.sourceItem === sourceItem &&
|
||||||
|
Object.keys(item.threads).includes(thread.toString()))) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -279,6 +279,8 @@ export const todoItems = computed(() =>
|
||||||
);
|
);
|
||||||
export const todoCategories = computed(() => categories.value.filter(c => ["urgent", "notify", "todo"].includes(c.priority) && Object.keys(c.activeItems).length > 0));
|
export const todoCategories = computed(() => categories.value.filter(c => ["urgent", "notify", "todo"].includes(c.priority) && Object.keys(c.activeItems).length > 0));
|
||||||
|
|
||||||
|
watch(todoCategories, console.log)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
items,
|
items,
|
||||||
items => {
|
items => {
|
||||||
|
|
71
src/utils.ts
71
src/utils.ts
|
@ -1,6 +1,6 @@
|
||||||
import { Ref, computed, ref, watch } from "vue";
|
import { Ref, computed, ref, watch } from "vue";
|
||||||
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import { ThreadRef, sources } from "./state";
|
import { MessageEvent, ThreadRef, items, 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> {
|
||||||
|
@ -50,6 +50,59 @@ export function setupSelectedThread(baseUrl: Ref<string>, route: RouteLocationNo
|
||||||
return sources.value[selected.source].items[selected.sourceItem].threads[selected.thread].title;
|
return sources.value[selected.source].items[selected.sourceItem].threads[selected.thread].title;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isRead = computed(() => {
|
||||||
|
const selected = selectedItem.value;
|
||||||
|
if (selected == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !items.value.some(item =>
|
||||||
|
item.source === selected.source &&
|
||||||
|
item.sourceItem === selected.sourceItem &&
|
||||||
|
Object.keys(item.threads).includes("" + selected.thread));
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleRead() {
|
||||||
|
const selected = selectedItem.value;
|
||||||
|
if (selected == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const index = items.value.findIndex(item =>
|
||||||
|
item.source === selected.source &&
|
||||||
|
item.sourceItem === selected.sourceItem &&
|
||||||
|
Object.keys(item.threads).includes("" + selected.thread));
|
||||||
|
if (isRead.value) {
|
||||||
|
const timeline = sources.value[selected.source].items[selected.sourceItem].threads[selected.thread].timeline;
|
||||||
|
const lastMessage = timeline.findLast(msg => msg.type === 'message') as MessageEvent | undefined;
|
||||||
|
const threadUpdated = timeline[timeline.length - 1]?.time ?? 0;
|
||||||
|
const thread = {
|
||||||
|
count: 1,
|
||||||
|
preview: lastMessage?.message ?? "",
|
||||||
|
updatedAt: threadUpdated,
|
||||||
|
contact: lastMessage?.contact
|
||||||
|
}
|
||||||
|
if (index == -1) {
|
||||||
|
items.value.push({
|
||||||
|
source: selected.source,
|
||||||
|
sourceItem: selected.sourceItem,
|
||||||
|
updatedAt: threadUpdated,
|
||||||
|
threads: { [selected.thread]: thread }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.value[index].threads[selected.thread] = thread;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index != -1) {
|
||||||
|
// Very inefficient, but we want to batch both updates at once
|
||||||
|
const newItems = JSON.parse(JSON.stringify(items.value));
|
||||||
|
delete newItems[index].threads[selected.thread];
|
||||||
|
if (Object.keys(newItems[index].threads).length === 0) {
|
||||||
|
newItems.splice(index, 1);
|
||||||
|
}
|
||||||
|
items.value = newItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectItem(source: number, sourceItem: number, thread: number) {
|
function selectItem(source: number, sourceItem: number, thread: number) {
|
||||||
const p = route.params;
|
const p = route.params;
|
||||||
const sourceInt = parseInt(Array.isArray(p.source) ? p.source[0] : p.source);
|
const sourceInt = parseInt(Array.isArray(p.source) ? p.source[0] : p.source);
|
||||||
|
@ -85,5 +138,17 @@ export function setupSelectedThread(baseUrl: Ref<string>, route: RouteLocationNo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { showThread, selectedItem, snoozingItem, snoozing, threadTitle, selectItem, snoozeItem, close, stopSnoozing };
|
return {
|
||||||
|
showThread,
|
||||||
|
selectedItem,
|
||||||
|
snoozingItem,
|
||||||
|
snoozing,
|
||||||
|
threadTitle,
|
||||||
|
selectItem,
|
||||||
|
snoozeItem,
|
||||||
|
close,
|
||||||
|
stopSnoozing,
|
||||||
|
isRead,
|
||||||
|
toggleRead
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue