Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 10 additions & 57 deletions packages/frontend-main/src/components/popups/NewPostDialog.vue
Original file line number Diff line number Diff line change
@@ -1,45 +1,25 @@
<script lang="ts" setup>
import { Decimal } from '@cosmjs/math';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';

import PostEditorToolbar from '@/components/posts/PostEditorToolbar.vue';
import PostMediaThumbnail from '@/components/posts/PostMediaThumbnail.vue';
import
{ Button }
from '@/components/ui/button';
import PostComposer from '@/components/posts/PostComposer.vue';
import {
Dialog,
DialogTitle,
ResponsiveDialogContent,
} from '@/components/ui/dialog';
import InputPhoton from '@/components/ui/input/InputPhoton.vue';
import { Textarea } from '@/components/ui/textarea';
import { useConfirmDialog } from '@/composables/useConfirmDialog';
import { useCreatePost } from '@/composables/useCreatePost';
import { useTxDialog } from '@/composables/useTxDialog';
import { useConfigStore } from '@/stores/useConfigStore';
import { fractionalDigits } from '@/utility/atomics';
import { showBroadcastingToast } from '@/utility/toast';

const MAX_CHARS = 512 - 'dither.Post("")'.length;
const message = ref('');
const isBalanceInputValid = ref(false);

const { createPost, txError, txSuccess } = useCreatePost();
const { showConfirmDialog } = useConfirmDialog();

const { isShown, inputPhotonModel, handleClose } = useTxDialog<object>('newPost', txSuccess, txError);
const configStore = useConfigStore();
const amountAtomics = computed(() => configStore.config.defaultAmountEnabled ? configStore.config.defaultAmountAtomics : Decimal.fromUserInput(inputPhotonModel.value.toString(), fractionalDigits).atomics);

function handleInputValidity(value: boolean) {
isBalanceInputValid.value = value;
}

const canSubmit = computed(() => {
return isBalanceInputValid.value && message.value.length > 0;
});
const { isShown, handleClose } = useTxDialog<object>('newPost', txSuccess, txError);

function handleCloseWithSaveDraft() {
handleClose();
Expand All @@ -57,12 +37,7 @@ function handleCloseWithSaveDraft() {
});
}

async function handleSubmit() {
if (!canSubmit.value) {
return;
}

const msgValue = message.value;
async function handleSubmit({ message: msgValue, amountAtomics }: { message: string; amountAtomics: string }) {
message.value = '';
handleClose();

Expand All @@ -71,25 +46,12 @@ async function handleSubmit() {
try {
await createPost({
message: msgValue,
amountAtomics: amountAtomics.value,
amountAtomics,
});
} finally {
toast.dismiss(toastId);
}
}

function handleInsertText(text: string) {
// Ensure there's a space before inserting new text
if (message.value.length > 0 && !message.value.endsWith(' ')) {
message.value += ' ';
}

message.value += text;
}

function handleRemoveText(text: string) {
message.value = message.value.replace(text, '').trim();
}
</script>

<template>
Expand All @@ -98,21 +60,12 @@ function handleRemoveText(text: string) {
<ResponsiveDialogContent>
<DialogTitle>{{ $t('components.PopupTitles.newPost') }}</DialogTitle>

<Textarea
v-model="message" :placeholder="$t('placeholders.post')" :maxlength="MAX_CHARS" class="min-h-[74px] w-full break-all"
<PostComposer
v-model="message"
:max-chars="MAX_CHARS"
:placeholder="$t('placeholders.post')"
@submit="handleSubmit"
/>

<PostMediaThumbnail :content="message" @remove-text="handleRemoveText" />

<PostEditorToolbar :content="message" @insert-text="handleInsertText" />

<!-- Transaction Form -->
<div class="flex flex-col w-full gap-4">
<InputPhoton v-if="!configStore.config.defaultAmountEnabled" v-model="inputPhotonModel" @on-validity-change="handleInputValidity" />
<Button class="w-full" :disabled="!canSubmit" @click="handleSubmit">
{{ $t('components.Button.submit') }}
</Button>
</div>
</ResponsiveDialogContent>
</Dialog>
</div>
Expand Down
49 changes: 19 additions & 30 deletions packages/frontend-main/src/components/popups/ReplyDialog.vue
Original file line number Diff line number Diff line change
@@ -1,67 +1,60 @@
<script lang="ts" setup>
import type { Post } from 'api-main/types/feed';

import { Decimal } from '@cosmjs/math';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';

import PostComposer from '@/components/posts/PostComposer.vue';
import PostMessage from '@/components/posts/PostMessage.vue';
import PrettyTimestamp from '@/components/posts/PrettyTimestamp.vue';
import { Button } from '@/components/ui/button';
import { Dialog, DialogTitle, ResponsiveDialogContent } from '@/components/ui/dialog';
import InputPhoton from '@/components/ui/input/InputPhoton.vue';
import { Textarea } from '@/components/ui/textarea';
import UserAvatar from '@/components/users/UserAvatar.vue';
import Username from '@/components/users/Username.vue';
import { useCreateReply } from '@/composables/useCreateReply';
import { useTxDialog } from '@/composables/useTxDialog';
import { useConfigStore } from '@/stores/useConfigStore';
import { fractionalDigits } from '@/utility/atomics';
import { showBroadcastingToast } from '@/utility/toast';

const POST_HASH_LEN = 64;
const MAX_CHARS = 512 - ('dither.Reply("", "")'.length + POST_HASH_LEN);
const message = ref('');
const isBalanceInputValid = ref(false);
const configStore = useConfigStore();

const { createReply, txError, txSuccess } = useCreateReply();
const {
isShown,
inputPhotonModel,
popupState: reply,
handleClose,
} = useTxDialog<Post>('reply', txSuccess, txError);
const amountAtomics = computed(() => configStore.config.defaultAmountEnabled ? configStore.config.defaultAmountAtomics : Decimal.fromUserInput(inputPhotonModel.value.toString(), fractionalDigits).atomics);
const canSubmit = computed(() => configStore.config.defaultAmountEnabled || (isBalanceInputValid.value && message.value.length > 0));

async function handleSubmit() {
if (!canSubmit.value || !reply.value) {
function handleCloseWithCleanup() {
message.value = '';
handleClose();
}

async function handleSubmit({ message: msgValue, amountAtomics }: { message: string; amountAtomics: string }) {
if (!reply.value) {
return;
}

const parentPost = ref(reply.value);
message.value = '';
handleClose();
const toastId = showBroadcastingToast('Reply');

try {
await createReply({ parentPost, message: message.value, amountAtomics: amountAtomics.value });
await createReply({ parentPost, message: msgValue, amountAtomics });
} finally {
toast.dismiss(toastId);
}
}

function handleInputValidity(value: boolean) {
isBalanceInputValid.value = value;
}
</script>

<template>
<div>
<Dialog v-if="isShown" open @update:open="handleClose">
<Dialog v-if="isShown" open @update:open="handleCloseWithCleanup">
<ResponsiveDialogContent>
<DialogTitle>{{ $t('components.PopupTitles.reply') }}</DialogTitle>

<!-- Parent Post Display -->
<div class="flex flex-row gap-3 border-b pb-3">
<UserAvatar :user-address="reply.author" />
<div class="flex flex-col w-full gap-3">
Expand All @@ -76,16 +69,12 @@ function handleInputValidity(value: boolean) {
</div>
</div>

<Textarea v-model="message" :placeholder="$t('placeholders.reply')" :maxlength="MAX_CHARS" />

<!-- Transaction Form -->
<div class="flex flex-col w-full gap-4">
<InputPhoton v-if="!configStore.config.defaultAmountEnabled" v-model="inputPhotonModel" @on-validity-change="handleInputValidity" />
<span v-if="txError" class="text-red-500 text-xs">{{ txError }}</span>
<Button class="w-full" :disabled="!canSubmit" @click="handleSubmit">
{{ $t('components.Button.submit') }}
</Button>
</div>
<PostComposer
v-model="message"
:max-chars="MAX_CHARS"
:placeholder="$t('placeholders.reply')"
@submit="handleSubmit"
/>
</ResponsiveDialogContent>
</Dialog>
</div>
Expand Down
107 changes: 107 additions & 0 deletions packages/frontend-main/src/components/posts/PostComposer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { Decimal } from '@cosmjs/math';
import { computed, nextTick, onMounted, ref } from 'vue';

import PostEditorToolbar from '@/components/posts/PostEditorToolbar.vue';
import PostMediaThumbnail from '@/components/posts/PostMediaThumbnail.vue';
import { Button } from '@/components/ui/button';
import InputPhoton from '@/components/ui/input/InputPhoton.vue';
import { Textarea } from '@/components/ui/textarea';
import { useConfigStore } from '@/stores/useConfigStore';
import { fractionalDigits } from '@/utility/atomics';

const props = defineProps<{
maxChars: number;
placeholder: string;
}>();

const emit = defineEmits<{
submit: [{ message: string; amountAtomics: string }];
}>();

const message = defineModel<string>({ required: true });

const isBalanceInputValid = ref(false);
const configStore = useConfigStore();
const inputPhotonModel = ref('0.1');
const textareaRef = ref<HTMLTextAreaElement>();

const amountAtomics = computed(() => {
if (configStore.config.defaultAmountEnabled) {
return configStore.config.defaultAmountAtomics;
}
return Decimal.fromUserInput(inputPhotonModel.value.toString(), fractionalDigits).atomics;
});

const canSubmit = computed(() => {
return isBalanceInputValid.value && message.value.length > 0;
});

const remainingChars = computed(() => props.maxChars - message.value.length);

function handleInputValidity(value: boolean) {
isBalanceInputValid.value = value;
}

function handleInsertText(text: string) {
// Ensure there's a space before inserting new text
if (message.value.length > 0 && !message.value.endsWith(' ')) {
message.value += ' ';
}

message.value += text;
}

function handleRemoveText(text: string) {
message.value = message.value.replace(text, '').trim();
}

function handleSubmit() {
if (!canSubmit.value) {
return;
}

emit('submit', {
message: message.value,
amountAtomics: amountAtomics.value,
});
}

onMounted(async () => {
await nextTick();
const textarea = textareaRef.value?.$el as HTMLTextAreaElement | undefined;
textarea?.focus();
});

defineExpose({
reset: () => {
message.value = '';
},
});
</script>

<template>
<div class="flex flex-col gap-4">
<Textarea
ref="textareaRef"
v-model="message" :placeholder="placeholder" class="min-h-[74px] max-h-[270px] md:max-h-[300px] w-full break-all overflow-y-auto resize-none"
/>

<PostMediaThumbnail :content="message" @remove-text="handleRemoveText" />

<div class="flex items-center justify-between border-t pt-2">
<PostEditorToolbar :content="message" @insert-text="handleInsertText" />
<span class="text-[11px]" :class="remainingChars < 0 ? 'text-red-500' : 'text-muted-foreground'">
{{ remainingChars }}
</span>
</div>

<!-- Transaction Form -->
<div class="flex flex-col w-full gap-4">
<InputPhoton v-if="!configStore.config.defaultAmountEnabled" v-model="inputPhotonModel" @on-validity-change="handleInputValidity" />
<Button class="w-full" :disabled="!canSubmit" @click="handleSubmit">
{{ $t('components.Button.submit') }}
</Button>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ function insertVideoUrl() {
</script>

<template>
<div class="flex gap-2 items-center border-t pt-2">
<div class="flex gap-2 items-center">
<!-- Image URL Popover -->
<ResponsivePopoverDialog v-model:open="isImagePopoverOpen" modal>
<template #trigger>
<Button
variant="ghost"
size="icon"
size="sm"
type="button"
:title="$t('components.PostEditorToolbar.insertImageTitle')"
:class="{ 'text-blue-500': hasImageInContent }"
class="px-1.5 py-1.5 h-auto" :class="[{ 'text-blue-500': hasImageInContent }]"
>
<Image class="size-5" />
<Image class="size-4" />
</Button>
</template>
<div class="flex flex-col gap-2">
Expand Down Expand Up @@ -93,12 +93,12 @@ function insertVideoUrl() {
<template #trigger>
<Button
variant="ghost"
size="icon"
size="sm"
type="button"
:title="$t('components.PostEditorToolbar.insertVideoTitle')"
:class="{ 'text-blue-500': hasVideoInContent }"
class="px-1.5 py-1.5 h-auto" :class="[{ 'text-blue-500': hasVideoInContent }]"
>
<Video class="size-5" />
<Video class="size-4" />
</Button>
</template>
<div class="flex flex-col gap-2">
Expand Down
Loading