Initial Commit
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
import Avatar from '@/components/dredge/Avatar.vue';
|
||||
import MessageText from './MessageText.vue';
|
||||
import MessageShare from './MessageShare.vue';
|
||||
import MessageStoryShare from './MessageStoryShare.vue';
|
||||
import MessageMedia from './MessageMedia.vue';
|
||||
import MessageSystem from './MessageSystem.vue';
|
||||
import type { ThreadMessage } from '@/types/dredge.types';
|
||||
|
||||
@@ -26,9 +28,16 @@ defineProps<{
|
||||
{{ message.replied_to_text }}
|
||||
</p>
|
||||
|
||||
<MessageShare v-if="message.shared_url" :message="message" />
|
||||
<MessageText v-else-if="message.text" :message="message" />
|
||||
<MessageSystem v-else :message="message" :is-own="isOwn" />
|
||||
<!-- Text is a universal concern shown alongside whatever else the
|
||||
message carries, not one branch of a type dispatch — a story
|
||||
reply, for instance, has both a caption you typed AND a
|
||||
share preview, and previously only one or the other rendered. -->
|
||||
<MessageText v-if="message.text" :message="message" />
|
||||
|
||||
<MessageStoryShare v-if="message.story_share" :message="message" />
|
||||
<MessageShare v-else-if="message.shared_url" :message="message" />
|
||||
<MessageMedia v-else-if="message.attachments?.length" :message="message" />
|
||||
<MessageSystem v-else-if="!message.text" :message="message" :is-own="isOwn" />
|
||||
|
||||
<div v-if="message.reactions?.length" class="bubble__reactions">
|
||||
<span v-for="(reaction, i) in message.reactions" :key="i" class="bubble__reaction">
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { proxyImage } from '@/lib/proxyImage';
|
||||
import type { ThreadMessage } from '@/types/dredge.types';
|
||||
|
||||
defineProps<{
|
||||
message: ThreadMessage;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="message.attachments?.length"
|
||||
class="message-media"
|
||||
:class="{ 'message-media--grid': message.attachments.length > 1 }"
|
||||
>
|
||||
<a
|
||||
v-for="(attachment, i) in message.attachments"
|
||||
:key="i"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img :src="proxyImage(attachment.url)" alt="" class="message-media__image" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message-media {
|
||||
display: flex;
|
||||
}
|
||||
.message-media > a {
|
||||
display: block;
|
||||
line-height: 0;
|
||||
}
|
||||
.message-media__image {
|
||||
display: block;
|
||||
max-width: 220px;
|
||||
max-height: 320px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.message-media--grid {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
max-width: 220px;
|
||||
}
|
||||
.message-media--grid .message-media__image {
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { proxyImage } from '@/lib/proxyImage';
|
||||
import StoryViewer from '@/components/dredge/Stories/StoryViewer.vue';
|
||||
import type { ThreadMessage } from '@/types/dredge.types';
|
||||
|
||||
const props = defineProps<{
|
||||
message: ThreadMessage;
|
||||
}>();
|
||||
|
||||
const isOpen = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="message.story_share!.exists_locally ? 'button' : 'a'"
|
||||
class="story-share"
|
||||
:class="{ 'story-share--large': message.story_share!.exists_locally }"
|
||||
v-bind="
|
||||
message.story_share!.exists_locally
|
||||
? { type: 'button' }
|
||||
: { href: message.shared_url ?? undefined, target: '_blank', rel: 'noopener noreferrer' }
|
||||
"
|
||||
@click="message.story_share!.exists_locally && (isOpen = true)"
|
||||
>
|
||||
<img
|
||||
v-if="message.shared_preview_image"
|
||||
:src="proxyImage(message.shared_preview_image)"
|
||||
alt=""
|
||||
class="story-share__image"
|
||||
/>
|
||||
<span v-if="message.shared_title || message.shared_caption" class="story-share__body">
|
||||
<span v-if="message.shared_title" class="story-share__title">
|
||||
{{ message.shared_title }}
|
||||
</span>
|
||||
<span v-if="message.shared_caption" class="story-share__caption">
|
||||
{{ message.shared_caption }}
|
||||
</span>
|
||||
</span>
|
||||
</component>
|
||||
|
||||
<StoryViewer
|
||||
v-if="isOpen && message.story_share"
|
||||
:username="message.story_share.username"
|
||||
:item-id="message.story_share.item_id"
|
||||
@close="isOpen = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.story-share {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.4rem;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.story-share__image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.story-share__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
.story-share__title {
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.story-share__caption {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A story we actually have locally: worth a much bigger, tappable preview
|
||||
instead of the compact link-card treatment used for everything else. */
|
||||
.story-share--large {
|
||||
flex-direction: column;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.story-share--large .story-share__image {
|
||||
width: 160px;
|
||||
height: 284px; /* ~9:16, matching Instagram's own story aspect ratio */
|
||||
border-radius: 10px;
|
||||
}
|
||||
.story-share--large .story-share__body {
|
||||
width: 160px;
|
||||
}
|
||||
.story-share--large .story-share__caption,
|
||||
.story-share--large .story-share__title {
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,9 @@ import type { StoryDetail } from '@/types/dredge.types';
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
// When set, opens this specific (possibly expired) story item directly
|
||||
// instead of the user's current live tray.
|
||||
itemId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -30,12 +33,15 @@ async function fetchStory() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await fetch(`/api/stories/${props.username}`);
|
||||
const url = props.itemId ? `/api/story-items/${props.itemId}` : `/api/stories/${props.username}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load story');
|
||||
detail.value = await response.json();
|
||||
currentIndex.value = detail.value?.start_index ?? 0;
|
||||
} catch (e) {
|
||||
error.value = 'Could not load this story.';
|
||||
error.value = props.itemId
|
||||
? 'This story is no longer available.'
|
||||
: 'Could not load this story.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ export interface ThreadReaction {
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface ThreadStoryShare {
|
||||
username: string;
|
||||
item_id: string;
|
||||
exists_locally: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadMessage {
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
@@ -90,9 +96,11 @@ export interface ThreadMessage {
|
||||
shared_preview_image: string | null;
|
||||
shared_title: string | null;
|
||||
shared_caption: string | null;
|
||||
attachments: { url: string; width: number | null; height: number | null }[] | null;
|
||||
reactions: ThreadReaction[] | null;
|
||||
replied_to_id: string | null;
|
||||
replied_to_text: string | null;
|
||||
story_share: ThreadStoryShare | null;
|
||||
}
|
||||
|
||||
export interface ThreadDetail {
|
||||
|
||||
Reference in New Issue
Block a user