Add reply-to function
Deploy Livetrail / deploy (push) Successful in 33s

This commit is contained in:
2026-09-06 14:20:55 +02:00
parent 842bb56fcc
commit 51837ef1ac
13 changed files with 446 additions and 158 deletions
+8 -3
View File
@@ -35,7 +35,9 @@ export default {
return {
feed: {
checkNewFeed: this.checkNewFeed,
toggle: this.toggle
toggle: this.toggle,
reply: this.reply,
findLinkedPost: this.findLinkedPost
}
};
},
@@ -90,7 +92,7 @@ export default {
getScrollElement() {
return this.$refs.feedSimpleBar?.scrollElement;
},
async findLinkedPost(sPostType, iPostId, pMapIsReady) {
async findLinkedPost(sPostType, iPostId, pMapIsReady=Promise.resolve()) {
let vPost = await this.findPost(sPostType, iPostId, true);
if(vPost) {
await pMapIsReady;
@@ -206,6 +208,9 @@ export default {
if(aoMarkers.length > 0) this.$emit('new-markers', aoMarkers);
this.$emit('request-last-update');
},
reply(oQuote) {
this.$refs.poster?.addQuote(oQuote);
},
toggle(bShow, iAnimDuration=500) {
this.isOpen = (typeof bShow == 'boolean')?bShow:(!this.isOpen);
this.$emit('toggle', this.isOpen, iAnimDuration);
@@ -223,7 +228,7 @@ export default {
<Simplebar id="feed-panel" class="panel-content" ref="feedSimpleBar" :aria-busy="loading">
<div id="feed-header">
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" ref="poster" />
</div>
<div v-if="project" v-show="!loadingPost" id="feed-posts">
<ProjectPost v-for="post in posts" :key="post.ref" :options="post" ref="posts" />
+68 -8
View File
@@ -5,6 +5,8 @@
import projectMediaLink from '@components/ProjectMediaLink';
import projectMapLink from '@components/ProjectMapLink';
import projectRelTime from '@components/ProjectRelTime';
import projectPostQuote from '@components/ProjectPostQuote';
import projectPostSignature from '@components/ProjectPostSignature';
import { LngLat } from 'maplibre-gl';
import { copyTextToClipboard } from '@scripts/common';
@@ -17,7 +19,9 @@
appButton,
projectMediaLink,
projectMapLink,
projectRelTime
projectRelTime,
projectPostQuote,
projectPostSignature
},
props: {
options: Object
@@ -31,9 +35,11 @@
anchorVisible: ['message', 'media', 'post'].includes(this.options.type),
anchorTitle: this.lang.get('post.copy_to_clipboard'),
anchorIcon: 'link',
replyTitle: this.lang.get('post.reply'),
popupRequested: false,
mouseOverDrill: false,
postMessage: '',
quote: null,
sending: false,
focusZoomLevel: 15
};
@@ -65,8 +71,13 @@
modeHisto() {
return (this.project?.project?.mode == this.consts.modes.histo);
},
replyVisible() {
return this.anchorVisible && !this.modeHisto;
},
relTime() {
return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time;
return this.modeHisto?
(this.options.formatted_time || '').substr(0, 10):
this.options.relative_time;
},
relatedMarker() {
//Find corresponding marker
@@ -88,9 +99,54 @@
},
messageLines() {
return (this.options.content || '').split('\n');
},
quoteRef() {
return this.options.ref_item?this.buildQuote(this.options.ref_item):null;
}
},
methods: {
buildQuote(oItem) {
let sContent = '', sImage = null;
switch(oItem.type) {
case 'post':
sContent = oItem.content || '';
break;
case 'media':
sContent = oItem.comment || this.lang.get('media.posted_on', oItem.posted_on_formatted_time);
sImage = oItem.thumb_path;
break;
case 'message':
sContent = oItem.lat_dms+' '+oItem.lon_dms;
sImage = oItem.static_img_url;
break;
}
return {
type: oItem.type,
id: oItem.id,
author: oItem.formatted_name || '',
gravatar: oItem.gravatar || null,
content: sContent,
image: sImage
};
},
reply() {
this.feed.reply(this.buildQuote(this.options));
},
goToQuote() {
if(this.quoteRef) this.feed.findLinkedPost(this.quoteRef.type, this.quoteRef.id);
},
addQuote(oQuote) {
this.quote = oQuote;
this.$nextTick(() => {
this.$refs.post.focus();
});
},
formatQuote(oQuote) {
return (oQuote.author?oQuote.author+':\n':'')
+ oQuote.content.split('\n').map((sLine) => '> '+sLine).join('\n');
},
copyAnchor() {
copyTextToClipboard(this.consts.server+this.anchorLink);
this.anchorTitle = this.lang.get('post.link_copied');
@@ -138,11 +194,14 @@
{
id_project: this.project.project.id,
name: this.user.name,
content: this.postMessage
content: this.postMessage,
ref_type: this.quote?.type || '',
ref_id: this.quote?.id || 0
}
)
.then(() => {
this.postMessage = '';
this.quote = null;
this.feed.checkNewFeed();
this.sending = false;
})
@@ -176,6 +235,9 @@
<div class="header" v-if="!options.headerless">
<div class="index">
<appIcon :icon="subType" :text="displayedId" width="auto" />
<a v-if="replyVisible" class="reply clickable" @click="reply" :title="replyTitle">
<appIcon :icon="'reply'" />
</a>
<a v-if="anchorVisible" class="link desktop" @click="copyAnchor" ref="anchor" :href="anchorLink" :title="anchorTitle">
<appIcon :icon="anchorIcon" />
</a>
@@ -210,16 +272,14 @@
<projectMediaLink :options="options" :type="'post'" ref="medialink" @opening-lightbox="panMapToMarker" />
</div>
<template v-else-if="options.type == 'post'">
<projectPostQuote v-if="quoteRef" :quote="quoteRef" class="clickable" @click="goToQuote" />
<div class="message">
<p v-for="(sLine, iIndex) in messageLines" :key="iIndex">{{ sLine }}</p>
</div>
<div class="signature">
<img v-if="options.gravatar" :src="'data:image/png;base64, '+options.gravatar" width="24" height="24" alt="--" />
<span v-else>-- </span>
<span>{{ options.formatted_name }}</span>
</div>
<projectPostSignature :name="options.formatted_name" :gravatar="options.gravatar" />
</template>
<div v-else-if="options.type == 'poster'" class="poster-form">
<projectPostQuote v-if="quote" :quote="quote" />
<textarea ref="post" name="post" :placeholder="lang.get('post.message')" class="autoExpand" rows="1" v-model="postMessage"></textarea>
<div class="poster-actions">
<input type="text" name="name" :placeholder="lang.get('post.name')" v-model="user.name" />
+22
View File
@@ -0,0 +1,22 @@
<script>
import projectPostSignature from '@components/ProjectPostSignature';
export default {
components: {
projectPostSignature
},
props: {
quote: Object
}
};
</script>
<template>
<div :class="'quote '+quote.type">
<img v-if="quote.image" class="image" :src="quote.image" />
<div class="text">
<p class="content">{{ quote.content }}</p>
<projectPostSignature v-if="quote.author" :name="quote.author" :gravatar="quote.gravatar" />
</div>
</div>
</template>
+29
View File
@@ -0,0 +1,29 @@
<script>
export default {
props: {
name: String,
gravatar: String
}
};
</script>
<template>
<div class="signature">
<img v-if="gravatar" :src="'data:image/png;base64, '+gravatar" width="24" height="24" alt="--" />
<span v-else>-- </span>
<span>{{ name }}</span>
</div>
</template>
<style scoped>
.signature {
text-align: right;
font-style: italic;
img {
vertical-align: baseline;
margin: 0 0.2em calc((1em - 24px)/2) 0;
position: relative;
}
}
</style>
+2
View File
@@ -45,6 +45,7 @@ import {
faPersonHiking,
faPhotoFilm,
faPlus,
faReply,
faScrewdriverWrench,
faShoePrints,
faSun,
@@ -134,6 +135,7 @@ const ICONS = {
'image-shot': faCamera,
link: faLink,
copied: faCheck,
reply: faReply,
'find-me-spot': findMeSpot,
/* Feed - Poster */
+65 -14
View File
@@ -4,6 +4,66 @@
#feed {
#feed-panel {
.quote {
display: flex;
align-items: center;
gap: var.$elem-spacing;
padding: var.$elem-spacing;
border-radius: var.$block-radius;
border-left-width: var.$border-thick;
border-left-style: solid;
font-size: 0.9em;
box-shadow: var.$elem-shadow;
&.post {
border-left-color: color.$post;
background: color.$post-bg;
color: color.$post;
}
&.message {
border-left-color: color.$message;
background: color.$message-bg;
color: color.$message;
}
&.media {
border-left-color: color.$media;
background: color.$media-bg;
color: color.$media;
}
.image {
flex: 0 0 auto;
width: 3 * var.$block-spacing;
height: 3 * var.$block-spacing;
object-fit: cover;
border-radius: var.$block-radius;
}
.text {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: var.$text-spacing;
.content {
margin: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
img {
transform: scaleX(90%) scaleY(90%);
}
}
}
#feed-header {
.poster {
.poster-form {
@@ -70,7 +130,7 @@
gap: var.$elem-spacing;
flex: 0 0 auto;
.link {
.link, .reply {
padding: 0;
line-height: 1;
}
@@ -203,7 +263,10 @@
&.post {
.body {
padding: 0 var.$block-spacing var.$elem-spacing;
display: flex;
flex-direction: column;
gap: var.$elem-spacing;
padding-bottom: var.$elem-spacing;
.message p {
margin: 0;
@@ -212,18 +275,6 @@
margin-top: var.$text-spacing;
}
}
.signature {
margin-top: var.$elem-spacing;
text-align: right;
font-style: italic;
img {
vertical-align: baseline;
margin: 0 0.2em calc((1em - 24px)/2) 0;
position: relative;
}
}
}
}
+5 -1
View File
@@ -14,4 +14,8 @@ $elem-shadow: 0px 1px 1px color.$over-img-shadow;
//Transitions
$trans-quick: 200ms;
$trans-slow: 500ms;
$zoom-scale: 1.15;
$zoom-scale: 1.15;
//Border
$border-thin: 1px;
$border-thick: 3px;