feat(ConversationViewer): add functionality for sharing messages as paper messages, implement translation feature, and enhance message input with draft saving and dynamic height adjustment

This commit is contained in:
2026-01-02 17:27:31 -06:00
parent ece1414079
commit 6db10e3e8f

View File

@@ -463,6 +463,15 @@
>
Delete
</button>
<!-- share as paper message -->
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-600 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 ml-2"
@click.stop="shareAsPaperMessage(chatItem)"
>
Paper Message
</button>
</div>
</div>
@@ -752,8 +761,9 @@
ref="message-input"
v-model="newMessageText"
:readonly="isSendingMessage"
class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 block w-full px-3 sm:px-4 py-2 resize-none shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
rows="2"
class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 block w-full px-3 sm:px-4 py-2 resize-none shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500 min-h-[40px] max-h-[200px] overflow-y-auto"
rows="1"
spellcheck="true"
:placeholder="$t('messages.send_placeholder')"
@keydown.enter.exact.prevent="onEnterPressed"
@keydown.enter.shift.exact.prevent="onShiftEnterPressed"
@@ -791,6 +801,16 @@
<MaterialDesignIcon icon-name="crosshairs-question" class="w-4 h-4" />
<span class="hidden sm:inline">{{ $t("messages.request") }}</span>
</button>
<button
v-if="hasTranslator && newMessageText"
type="button"
class="attachment-action-button"
:title="$t('translator.translate')"
@click="translateMessage"
>
<MaterialDesignIcon icon-name="translate" class="w-4 h-4" />
<span class="hidden sm:inline">{{ $t("translator.translate") }}</span>
</button>
<div class="ml-auto my-auto">
<SendMessageButton
:is-sending-message="isSendingMessage"
@@ -885,6 +905,12 @@
</div>
</div>
</Transition>
<PaperMessageModal
v-if="isPaperMessageModalOpen"
:message-hash="paperMessageHash"
@close="isPaperMessageModalOpen = false"
/>
</template>
<script>
@@ -905,6 +931,7 @@ import AddImageButton from "./AddImageButton.vue";
import IconButton from "../IconButton.vue";
import GlobalEmitter from "../../js/GlobalEmitter";
import ToastUtils from "../../js/ToastUtils";
import PaperMessageModal from "./PaperMessageModal.vue";
export default {
name: "ConversationViewer",
@@ -915,6 +942,7 @@ export default {
MaterialDesignIcon,
SendMessageButton,
AddAudioButton,
PaperMessageModal,
},
props: {
myLxmfAddressHash: {
@@ -982,6 +1010,10 @@ export default {
0x08: "2400", // AM_CODEC2_2400
0x09: "3200", // AM_CODEC2_3200
},
isPaperMessageModalOpen: false,
paperMessageHash: null,
hasTranslator: false,
translatorLanguages: [],
};
},
computed: {
@@ -1042,9 +1074,15 @@ export default {
},
watch: {
selectedPeer: {
handler() {
handler(newPeer, oldPeer) {
if (oldPeer) {
this.saveDraft(oldPeer.destination_hash);
}
this.checkIfSelectedPeerBlocked();
this.initialLoad();
if (newPeer) {
this.loadDraft(newPeer.destination_hash);
}
},
immediate: true,
},
@@ -1052,6 +1090,11 @@ export default {
// chat items for selected peer changed, so lets process any available audio
await this.processAudioForSelectedPeerChatItems();
},
newMessageText() {
this.$nextTick(() => {
this.adjustTextareaHeight();
});
},
},
beforeUnmount() {
// stop listening for websocket messages
@@ -1067,6 +1110,9 @@ export default {
// load blocked destinations
this.loadBlockedDestinations();
// check translator
this.checkTranslator();
},
methods: {
async loadBlockedDestinations() {
@@ -1078,6 +1124,48 @@ export default {
console.log(e);
}
},
async checkTranslator() {
try {
const response = await window.axios.get("/api/v1/translator/languages");
this.translatorLanguages = response.data.languages || [];
this.hasTranslator = this.translatorLanguages.length > 0;
} catch (e) {
console.log("Failed to check translator:", e);
this.hasTranslator = false;
}
},
async translateMessage() {
if (!this.newMessageText || this.isSendingMessage) return;
try {
this.isSendingMessage = true;
const targetLang = this.$i18n.locale || "en";
const response = await window.axios.post("/api/v1/translator/translate", {
text: this.newMessageText,
source_lang: "auto",
target_lang: targetLang,
});
if (response.data.translated_text) {
this.newMessageText = response.data.translated_text;
this.$nextTick(() => {
this.adjustTextareaHeight();
});
}
} catch (e) {
console.error("Translation failed:", e);
ToastUtils.error("Translation failed");
} finally {
this.isSendingMessage = false;
}
},
adjustTextareaHeight() {
const textarea = this.$refs["message-input"];
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = Math.min(textarea.scrollHeight, 200) + "px";
}
},
checkIfSelectedPeerBlocked() {
if (!this.selectedPeer) {
this.isSelectedPeerBlocked = false;
@@ -1087,6 +1175,30 @@ export default {
(b) => b.destination_hash === this.selectedPeer.destination_hash
);
},
loadDraft(destinationHash) {
try {
const drafts = JSON.parse(localStorage.getItem("meshchat.drafts") || "{}");
this.newMessageText = drafts[destinationHash] || "";
this.$nextTick(() => {
this.adjustTextareaHeight();
});
} catch (e) {
console.error("Failed to load draft:", e);
}
},
saveDraft(destinationHash) {
try {
const drafts = JSON.parse(localStorage.getItem("meshchat.drafts") || "{}");
if (this.newMessageText) {
drafts[destinationHash] = this.newMessageText;
} else {
delete drafts[destinationHash];
}
localStorage.setItem("meshchat.drafts", JSON.stringify(drafts));
} catch (e) {
console.error("Failed to save draft:", e);
}
},
close() {
this.$emit("close");
},
@@ -1713,6 +1825,10 @@ export default {
this.isShareContactModalOpen = false;
await this.sendMessage();
},
shareAsPaperMessage(chatItem) {
this.paperMessageHash = chatItem.lxmf_message.hash;
this.isPaperMessageModalOpen = true;
},
async sendMessage() {
// do nothing if can't send message
if (!this.canSendMessage) {
@@ -1814,6 +1930,7 @@ export default {
// clear message inputs
this.newMessageText = "";
this.saveDraft(this.selectedPeer.destination_hash);
this.newMessageImage = null;
this.newMessageImageUrl = null;
this.newMessageAudio = null;