tweaked mentions, feed, following

This commit is contained in:
monica 2022-05-28 10:25:59 -05:00
parent 59a77368c6
commit ec3d3396d4
17 changed files with 391 additions and 223 deletions

View File

@ -35,6 +35,7 @@
"tributejs": "^5.1.3",
"vue": "^3.0.0",
"vue-router": "^4.0.0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.1"
},
"devDependencies": {

View File

@ -15,13 +15,6 @@
<q-tooltip>
{{isFollowing ? "unfollow" : "follow" }}
</q-tooltip>
<q-icon
:name='isFollowing ? "person_remove" : "person_add"'
:class='isFollowing ? "flip-horizontal" : ""'
/>
<q-tooltip>
{{isFollowing ? "unfollow" : "follow" }}
</q-tooltip>
</q-btn>
</template>

View File

@ -127,6 +127,9 @@ div#emoji-mart-list section:first-of-type h3:first-of-type {
.emoji-mart-emoji {
padding: .3rem;
}
.emoji-mart-emoji:hover:before {
display: none;
}
button.emoji-mart-emoji {
align-items: center;
justify-content: flex-start;

View File

@ -205,12 +205,18 @@ ul {
padding-inline-start: 1rem;
text-align: left;
}
.post-highlighted ul {
padding-inline-start: 1.5rem;
}
ol {
list-style-type: decimal;
list-style-position: outside;
padding-inline-start: 1rem;
text-align: left;
}
.post-highlighted ol {
padding-inline-start: 1.5rem;
}
ul ul,
ol ul {
list-style-type: circle;
@ -226,7 +232,6 @@ ul ol {
p {
display: inline;
}
.break-word-wrap {
overflow-wrap: break-word;
word-wrap: break-word;

View File

@ -30,14 +30,15 @@
/>
</div>
<!-- <q-list id='tribute-wrapper' class='overflow-auto' style='position: aboslute; bottom: 100%; max-height: 70vh;'> -->
<q-list id='tribute-wrapper' class='overflow-auto flex row z-top' style='max-height: 70vh' @click.stop>
<q-list id='tribute-wrapper' class='overflow-auto flex row z-top' style='max-height: 70vh' @click.stop='focusInput'>
</q-list>
<q-list v-if='Object.keys(mentions).length && !sending && focus'>
<div class='text-bold text-caption'>tags<span v-if='$route.name === "messages"'>{{' **NOTE TAGS ARE NOT PRIVATE**'}}</span></div>
<div v-for='(mention, index) in mentions' :key='index' class='flex row no-wrap'>
{{ "#[" + mention.index + "] "+ (mention.tag[0] === "e" ? " event: " : "") + (mention.tag[0] === "p" ? " profile: " : "")}}
<BaseUserName v-if='mention.tag[0] === "p"' :pubkey='mention.tag[1]' :fallback='true'/>
<BaseMarkdown v-if='mention.tag[0] === "e"'> {{ `[&${shorten(mention.tag[1])}](/event/${mention.tag[1]})` }} </BaseMarkdown>
<q-list v-if='tags.length && !sending' class='q-px-sm tagged-wrapper'>
<div class='text-bold text-subtitle2 text-primary'>tagged<span v-if='$route.name === "messages"'>{{' **NOTE TAGS ARE NOT PRIVATE**'}}</span></div>
<div v-for='(tag, index) in tags' :key='index' class='flex row no-wrap q-gutter-xs'>
<div class='text-bold'>{{ "#[" + index + "] " }}</div>
<div>{{ (tag[0] === "e" ? " event: " : "") + (tag[0] === "p" ? " user: " : "")}}</div>
<BaseUserName v-if='tag[0] === "p"' :pubkey='tag[1]' :fallback='true'/>
<BaseMarkdown v-if='tag[0] === "e"'> {{ `[&${shorten(tag[1])}](/event/${tag[1]})` }} </BaseMarkdown>
</div>
</q-list>
<q-form
@ -67,19 +68,14 @@
autogrow
autofocus
:label="label"
:disable='sending'
:disable='sending || mentionsUpdating'
:loading='mentionsUpdating'
@update='updateCursorPosition'
@keypress.ctrl.enter="send"
@keyup='updateCursorPosition'
@keydown='updateCursorPosition'
@delete='updateCursorPosition'
@click='updateCursorPosition'
@focus='focus = true'
@blur='focus = false'
@click='trigger++'
@keyup='trigger++'
>
<template #loading>
<div class='row justify-center q-my-md'>
<div class='full-width row justify-center q-my-md'>
<q-spinner-orbit color="accent" size='md'/>
</div>
</template>
@ -167,6 +163,7 @@
<script>
import helpersMixin from '../utils/mixin'
// import {getPubKeyTagWithRelay, getEventTagWithRelay, processMentions} from '../utils/helpers'
// import {nextTick} from 'vue'
import {getPubKeyTagWithRelay, getEventTagWithRelay, extractMentions} from '../utils/helpers'
import BaseButtonCopy from 'components/BaseButtonCopy.vue'
import BaseButtonClear from 'components/BaseButtonClear.vue'
@ -212,7 +209,7 @@ export default {
data() {
return {
text: '',
cursorPosition: 0,
// cursorPosition: 0,
sending: false,
// emojiSelecting: false,
toolSelected: '',
@ -220,32 +217,40 @@ export default {
sendIconTranslation: 0,
// tributeList: [],
tags: [],
focus,
// focus,
mentionsUpdating: false,
focusInput() {
this.$refs.input.focus()
setTimeout(async () => {
await this.$nextTick()
this.$refs.input.focus()
}, 1)
},
trigger: 1,
}
},
watch: {
mentions(curr, prev) {
if (Object.keys(curr).length < Object.keys(prev).length) {
// await this.$nextTick()
// this.trigger++
this.recalibrateMentionTags()
}
},
'text'(curr, prev) {
if (curr.length > prev.length) {
this.updateMentionsTags()
}
},
'replyMode'(curr, prev) {
if (this.replyMode && curr !== prev) {
if (curr !== prev) {
this.$emit('resized')
setTimeout(async () => {
await this.$nextTick()
this.focusInput()
}, 20)
this.focusInput()
}
},
'messageMode'(curr, prev) {
if (this.messageMode && curr !== prev) {
// this.focusInput()
// await this.$nextTick()
setTimeout(async () => {
await this.$nextTick()
this.focusInput()
}, 20)
if (curr !== prev) {
this.focusInput()
}
},
},
@ -258,7 +263,6 @@ export default {
return this.createMentionsProvider()
},
overCharLimit() {
// if (this.messageMode) false
return 280 - this.text.length < 0
},
textValid() {
@ -326,6 +330,16 @@ export default {
}
return hashtags
},
cursorPosition() {
// only checking this.text.length to trigger recompute
if (this.text.length && this.trigger) return this.textarea.selectionStart
else return this.textarea.selectionStart
},
cursorPositionEnd() {
// only checking this.text.length to trigger recompute
if (this.text.length && this.trigger) return this.textarea.selectionEnd
else return this.textarea.selectionEnd
},
},
mounted() {
@ -334,6 +348,7 @@ export default {
beforeUnmount() {
this.profileMentionsProvider.detach(this.textarea)
this.reset()
},
methods: {
@ -346,40 +361,17 @@ export default {
console.log('send already in progress')
return
}
this.cursorPosition = 0
this.toolSelected = ''
this.sending = true
this.animateSendIcon()
this.text = await extractMentions(this.text, this.tags)
// make sure mentions are sequential
if (Object.keys(this.mentions).length &&
Math.max(...Object.keys(this.mentions).map(key => key.split('_')[0])) >
this.tags.filter(tag => tag.length >= 2).filter((v, i, a) => a.indexOf(v) === i).length) {
// save copy of mentions and remove for now
let mentions = Object.assign({}, this.mentions)
this.tags = []
let offset = 0
console.log('fyi having to clean up too many mentions')
// now add back mentions
for (let index in mentions) {
let mention = mentions[index]
let idx = this.tags.findIndex(([t, v]) => t === mention.tag[0] && v === mention.tag[1])
// console.log('idx', idx)
if (idx === -1) {
this.tags.push(mention.tag)
idx = this.tags.length - 1
}
this.text = this.text.slice(0, mention.position + offset) + idx + this.text.slice(mention.position + offset + mention.length)
if (mention.length !== String(idx).length) offset = String(idx).length - mention.length
}
// }
}
this.recalibrateMentionTags()
let event
if (this.replyMode) event = await this.sendReply()
else if (this.messageMode) event = await this.sendMessage()
else event = await this.sendPost()
if (event) {
this.interpolateEventMentions(event)
if (!this.messageMode) this.interpolateEventMentions(event)
this.reset()
this.$emit('sent', event)
if (this.messageMode) this.$emit('clear-event')
@ -391,7 +383,7 @@ export default {
async sendPost() {
this.appendHashtags()
let tags = this.tags.map(([...v]) => [...v])
// console.log('tags sendPost:', tags)
// console.log('tags sendPost:', tags, this.tags)
let event = await this.$store.dispatch('sendPost', {message: this.text, tags: tags})
if (event) {
return event
@ -411,7 +403,6 @@ export default {
let usableTags = this.event.tags.filter(
([t, v]) => (t === 'p' || t === 'e') && v
).map(([t, v]) => { return [t, v] })
// console.log('usableTags: ', usableTags)
// add last 4 pubkeys mentioned
let pubkeys = usableTags.filter(([t, v]) => t === 'p').map(([_, v]) => v)
@ -419,7 +410,6 @@ export default {
for (let i = 0; i < Math.min(4, pubkeys.length); i++) {
this.tags.push(await getPubKeyTagWithRelay(pubkeys[pubkeys.length - 1 - i]))
}
// console.log('tags: ', tags)
// plus the author of the note being replied to, if not present already
if (!this.tags.find(([_, v]) => v === this.event.pubkey)) {
this.tags.push(await getPubKeyTagWithRelay(this.event.pubkey))
@ -437,7 +427,6 @@ export default {
let last = getEventTagWithRelay(this.event)
this.tags.push(last)
}
// console.log('tags: ', tags)
// remove ourselves
this.tags = this.tags.filter(([_, v]) => v !== this.$store.state.keys.pub)
@ -459,19 +448,10 @@ export default {
}
this.appendHashtags()
let tags = this.tags.map(([...v]) => [...v])
// let tags = this.tags.map((tag) => {
// if (tag.length >= 3) return [tag[0], tag[1], tag[2]]
// else return [tag[0], tag[1]]
// })
// console.log('text: ', this.text)
// tags.push(...this.hashtags)
// console.log('tags: ', tags)
// console.log('event', event)
return await this.$store.dispatch('sendPost', {
message: this.text,
tags: tags
})
// 07e4f77f3f1c8342f4627381832c4b796d7795e2355e5bba5eef672ee65e1d20
},
async sendMessage() {
@ -492,35 +472,30 @@ export default {
})
},
async updateCursorPosition() {
// console.log('mentions', this.mentions)
// console.log('tags', this.tags)
async updateMentionsTags() {
// let endOffset = this.text.length - this.cursorPosition
let curPos = this.cursorPosition
let prevTextLength = this.text.length
// console.log('updateMentionsTags cursor pos start, end', curPos, this.tags)
// if (curPos !== curPosEnd) return
const mentionRegex = /(?<t>[@&]{1})(?<p>[a-f0-9]{64})/g
if (this.text.match(mentionRegex)) {
if (this.text.toLowerCase().match(mentionRegex)) {
// console.log('mention found', this.text.length, this.cursorPosition)
this.mentionsUpdating = true
this.text = await extractMentions(this.text, this.tags)
this.mentionsUpdating = false
this.focusInput()
this.setCursorPosition(curPos + (this.text.length - prevTextLength))
}
// let interpolated = this.interpolateMentions(this.text, this.tags)
// this.tags = interpolated.
let cursorPos = this.$refs.input.$el.querySelector('textarea').selectionStart
if (cursorPos) {
this.cursorPosition = cursorPos
}
// const mentionAnchorRegex = /#\[/g
// const mentionAnchorRegex = /#\[(\d+)\]/g
// let matches = this.text.matchAll(mentionAnchorRegex)
// let mentions = {}
// for (let match in this.text.matchAll(mentionAnchorRegex)) {
// console.log('match', match)
// mentions[match.group.i] = this.tags[match.group.i]
// }
},
insertEmoji(emoji) {
this.text = this.text.slice(0, this.cursorPosition) + emoji.native + this.text.slice(this.cursorPosition)
this.cursorPosition += emoji.native.length
// this.cursorPosition++
let curPos = this.cursorPosition
let text = this.text
text = text.slice(0, curPos) + emoji.native + text.slice(curPos)
this.text = text
this.setCursorPosition(curPos + emoji.native.length)
this.focusInput()
},
animateSendIcon() {
@ -552,10 +527,6 @@ export default {
this.tags = []
},
upshiftTags(tags) {
if (this.tags.length === 0) this.tags.concat(tags)
},
appendHashtags() {
for (let hashtag of this.hashtags) {
if (!this.tags.find(([_, v]) => v === hashtag)) {
@ -563,6 +534,49 @@ export default {
}
}
},
recalibrateMentionTags() {
let curPos = this.cursorPosition
// console.log('recalibrateMentionTags cursor pos start, end', curPos, this.text.length)
let mentions = Object.assign({}, this.mentions)
// if (Object.keys(mentions).length === 0 && this.tags.length === 0) return
this.tags = []
let offset = 0
let text = this.text
// now add back mentions
for (let index in mentions) {
let mention = mentions[index]
let idx = this.tags.findIndex(([t, v]) => t === mention.tag[0] && v === mention.tag[1])
if (idx === -1) {
this.tags.push(mention.tag)
idx = this.tags.length - 1
}
if (String(idx) === mention.tag[1]) {
continue
}
text = text.slice(0, mention.position + offset) + idx + text.slice(mention.position + offset + mention.length)
if (mention.length !== String(idx).length) {
offset += String(idx).length - mention.length
if (mention.position + mention.length < curPos) {
// await this.setCursorPosition(curPos + String(idx).length - mention.length)
curPos += String(idx).length - mention.length
}
}
}
if (this.text !== text) {
this.text = text
this.setCursorPosition(curPos)
}
},
setCursorPosition(pos) {
// console.log('setting cursor position to ', pos)
setTimeout(async () => {
this.textarea.setSelectionRange(pos, pos)
this.trigger++
// console.log('checking cursor position', this.cursorPosition)
}, 1)
},
}
}
</script>
@ -597,7 +611,16 @@ export default {
</style>
<style lang='scss'>
ul {
#tribute-wrapper ul {
list-style-type: none;
}
#tribute-wrapper .tribute-container {
width: 100%;
}
#tribute-wrapper .tribute-container .highlight {
background: rgba(255, 255, 255, 0.1);
}
</style>

View File

@ -52,15 +52,46 @@
<q-separator color='accent' />
</div>
</q-card-section>
<h2 class='text-h5 text-bold q-my-none'> following </h2>
<div class='flex row justify-between no-wrap'>
<h2 class='text-h5 text-bold q-my-none'> following </h2>
<div>
<q-btn v-if='!reordering' flat icon='reorder' @click.stop='reorderFollowing'>
<q-tooltip>reorder following list</q-tooltip>
</q-btn>
<q-btn v-if='reordering' flat icon='close' @click.stop='cancelReorder'>
<q-tooltip>cancel</q-tooltip>
</q-btn>
</div>
</div>
<q-card-section class='no-padding' style='overflow-y: auto;'>
<q-list v-if="$store.state.following.length" class='q-mt-xs q-pl-sm'>
<BaseUserCard
v-for="pubkey in $store.state.following"
:pubkey="pubkey"
:key="pubkey"
/>
</q-list>
<div v-if='$store.state.following.length' class='q-mt-xs q-pl-sm'>
<q-list v-if="!reordering">
<BaseUserCard
v-for="pubkey in $store.state.following"
:pubkey="pubkey"
:key="pubkey"
/>
</q-list>
<Draggable
v-else-if='reorderedFollowing.length'
v-model='reorderedFollowing'
@start="dragging=true"
@end="dragging=false"
item-key="pubkey"
>
<!-- <div>{{element.name}}</div> -->
<template #header>
<div class='flex row justify-between items-start'>
<span>drag and drop to reorder</span>
<q-btn outline size='sm' icon='save' label='save' color='secondary' @click.stop='saveReorder'/>
</div>
</template>
<template #item="{element}">
<BaseUserCard :pubkey='element.pubkey' :action-buttons='false'/>
</template>
<!-- <BaseUserCard :clickable='false' :pubkey="element.pubkey" /> -->
</Draggable>
</div>
<div v-else>
When you follow someone they will show up here.
</div>
@ -71,6 +102,7 @@
<script>
import { defineComponent } from 'vue'
import {Notify} from 'quasar'
import Draggable from 'vuedraggable'
import {searchDomain, queryName} from 'nostr-tools/nip05'
import helpersMixin from '../utils/mixin'
import BaseButtonClear from 'components/BaseButtonClear.vue'
@ -85,11 +117,15 @@ export default defineComponent({
searching: false,
domainMode: false,
domainNames: {},
reordering: false,
reorderedFollowing: [],
dragging: false,
}
},
components: {
BaseButtonClear,
Draggable,
},
computed: {
@ -176,6 +212,22 @@ export default defineComponent({
message: 'No user found! Please enter full public key or NIP05 identifier and double check search string',
color: 'negative'
})
},
reorderFollowing() {
this.reorderedFollowing = this.$store.state.following.map((pubkey) => { return {pubkey} })
this.reordering = true
},
saveReorder() {
this.$store.commit('reorderFollows', this.reorderedFollowing.map(follow => follow.pubkey))
this.reordering = false
this.reorderedFollowing = []
},
cancelReorder() {
this.reordering = false
this.reorderedFollowing = []
}
}
})

View File

@ -100,9 +100,9 @@
transition-show='slide-up'
transition-hide='slide-down'
>
<q-card unelevated class='column postEntry'
<q-card unelevated class='flex column no-wrap post-entry'
>
<div class='flex row justify-end q-pa-sm'>
<div class='flex row justify-end'>
<q-btn icon="close" flat dense v-close-popup/>
</div>
<BasePostEntry class='q-pa-md' @sent='post = false'/>
@ -215,9 +215,9 @@ const userMenuItems = [
opacity: 1;
font-weight: bold;
}
.q-dialog .postEntry {
.q-dialog .post-entry {
width: 600px;
overflow: visible;
overflow: auto;
}
.compact-user-menu-space {
height: 2rem;

View File

@ -17,6 +17,7 @@
>
<q-tab name="follows" label='follows' />
<q-tab name="global" label='global' />
<q-tab v-if='botsFeed.length' name="bots" label='bots' />
</q-tabs>
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="follows" class='no-padding'>
@ -93,6 +94,43 @@
</div>
</div>
</q-tab-panel>
<q-tab-panel v-if='botsFeed.length' name="bots" class='no-padding'>
<div>
<q-virtual-scroll :items='botsFeed' virtual-scroll-item-size="110" ref='botsFeedScroll'>
<template #default="{ item }">
<BasePostThread :key="item[0].id" :events="item" @add-event='addEventGlobal'/>
</template>
</q-virtual-scroll>
<div v-if='botsFeed.length'>
<q-separator color='accent'/>
<q-btn-group
flat
spread
dense
text-color="accent"
>
<q-btn
dense
:loading='loadingMore'
flat
color="accent"
class='text-weight-light'
style='letter-spacing: .1rem;'
label='load another day'
@click="loadMoreGlobalFeed"
>
<template #loading>
<div class='row justify-center q-my-md'>
<q-spinner-orbit color="accent" size='md' />
</div>
</template>
</q-btn>
</q-btn-group>
<q-separator color='accent'/>
</div>
</div>
</q-tab-panel>
</q-tab-panels>
</q-page>
</template>
@ -115,6 +153,9 @@ export default {
followsFeedSet: new Set(),
globalFeed: [],
globalFeedSet: new Set(),
botsFeed: [],
botsFeedSet: new Set(),
bots: [],
loadingMore: false,
tab: 'follows',
sub: null,
@ -125,15 +166,6 @@ export default {
async mounted() {
this.loadMoreFollowsFeed()
this.loadMoreGlobalFeed()
// let notes = await dbGetHomeFeedNotes(200)
// this.interpolateEventMentions(notes)
// if (notes.length === 0) this.tab = 'global'
// if (notes.length > 0) this.reachedEnd = false
// for (let i = notes.length - 1; i >= 0; i--) {
// addToThread(this.followsFeed, notes[i])
// this.followsFeedSet.add(notes[i].id)
// }
this.listener = onNewHomeFeedNote(event => {
if (this.followsFeedSet.has(event.id)) return
@ -183,40 +215,29 @@ export default {
this.loadingMore = false
},
// listenGlobalFeed() {
// this.globalFeed = []
// this.globalFeedSet = new Set()
// this.sub = pool.sub(
// {
// filter: [
// {
// kinds: [1, 2],
// since: Math.floor(Date.now() / 1000) - 86400,
// until: Math.floor(Date.now() / 1000)
// }
// ],
// cb: async (event, relay) => {
// if (this.globalFeedSet.has(event.id)) return
// // this.$store.dispatch('useProfile', {
// // pubkey: event.pubkey,
// // request: true
// // })
// this.interpolateEventMentions(event)
// this.globalFeedSet.add(event.id)
// addToThread(this.globalFeed, event, 'feed')
// return
// }
// },
// 'global-feed'
// )
// },
loadMoreGlobalFeed() {
async loadMoreGlobalFeed() {
this.loadingMore = true
if (this.sub) this.sub.unsub()
if (this.bots.length === 0) {
await new Promise(resolve => {
let sub = pool.sub({
filter: [{authors: ['29f63b70d8961835b14062b195fc7d84fa810560b36dde0749e4bc084f0f8952'], kinds: [3]}],
cb: async event => {
this.bots = event.tags.filter(([t, v]) => t === 'p' && v).map(([_, v]) => v)
clearTimeout(timeout)
if (sub) sub.unsub()
resolve()
}
})
let timeout = setTimeout(() => {
sub.unsub()
sub = null
resolve()
}, 3000)
})
}
if (!this.since) this.since = Math.floor(Date.now() / 1000) - 86400
else this.since -= 86400
@ -238,7 +259,8 @@ export default {
// })
this.interpolateEventMentions(event)
this.globalFeedSet.add(event.id)
addToThread(this.globalFeed, event, 'feed')
if (this.bots.includes(event.pubkey)) addToThread(this.botsFeed, event)
else addToThread(this.globalFeed, event, 'feed')
return
}
},
@ -259,7 +281,9 @@ export default {
if (this.globalFeedSet.has(event.id)) return
this.interpolateEventMentions(event)
this.globalFeedSet.add(event.id)
addToThread(this.globalFeed, event, 'feed')
// addToThread(this.globalFeed, event, 'feed')
if (this.bots.includes(event.pubkey)) addToThread(this.botsFeed, event)
else addToThread(this.globalFeed, event, 'feed')
},
}
}

View File

@ -73,7 +73,12 @@
@click.stop='scrollToBottom()'
/>
<!-- <q-separator v-if='Object.keys(replyEvent).length' color='primary' size='1px'/> -->
<BasePostEntry :message-mode='replyEvent? "reply" : "message"' :event='replyEvent' @clear-event='replyEvent=null'/>
<BasePostEntry
:message-mode='replyEvent? "reply" : "message"'
:event='replyEvent'
@sent='addMessage'
@clear-event='replyEvent=null'
/>
</div>
</q-page>
</template>
@ -148,41 +153,7 @@ export default {
this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey})
this.listener = onNewMessage(this.$route.params.pubkey, async event => {
if (this.messagesSet.has(event.id)) return
this.messagesSet.add(event.id)
await this.lock()
event.text = await this.getPlaintext(event)
this.unlock()
this.interpolateMessageMentions(event)
if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
let messageScroll = this.$refs.messageScroll
let scrollToBottom = 100 > Math.abs((messageScroll.scrollHeight - messageScroll.clientHeight) - messageScroll.scrollTop) ||
messageScroll.scrollHeight === messageScroll.clientHeight
if (this.messages.length === 0) {
this.messages.push(event)
} else {
let last = this.messages[this.messages.length - 1]
if (
event.pubkey === this.$store.state.keys.pub &&
last.pubkey === event.pubkey &&
last.created_at + 120 >= event.created_at
) {
last.appended = last.appended || []
last.appended.push(event)
} else {
this.messages.push(event)
}
}
if (scrollToBottom) {
this.$store.commit('haveReadMessage', this.$route.params.pubkey)
this.scrollToBottom()
} else if (event.pubkey === this.$route.params.pubkey) {
this.unreadMessagesSet.add(event.id)
}
this.addMessage(event)
})
},
@ -208,28 +179,44 @@ export default {
this.canLoadMore = false
}
newMessages = newMessages.filter(event => !this.messagesSet.has(event.id))
// newMessages = newMessages.filter(event => !this.messagesSet.has(event.id))
let newMessagesFiltered = []
for (let i = 0; i < newMessages.length; i++) {
this.messagesSet.add(newMessages[i].id)
newMessages[i].text = await this.getPlaintext(newMessages[i])
this.interpolateMessageMentions(newMessages[i])
if (newMessages[i].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(newMessages[i])
if (newMessages[i].appended) {
for (let j = 0; j < newMessages[i].appended.length; j++) {
this.messagesSet.add(newMessages[i].appended[j].id)
newMessages[i].appended[j].text = await this.getPlaintext(newMessages[i].appended[j])
this.interpolateMessageMentions(newMessages[i].appended[j])
if (newMessages[i].appended[j].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(newMessages[i].appended[j])
// await newMessages.forEach(async (event) => {
let event = newMessages[i]
if (this.messagesSet.has(event.id)) return
this.messagesSet.add(event.id)
event.text = await this.getPlaintext(event)
this.interpolateMessageMentions(event)
if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
if (event.appended) {
for (let j = 0; j < event.appended.length; j++) {
this.messagesSet.add(event.appended[j].id)
event.appended[j].text = await this.getPlaintext(event.appended[j])
this.interpolateMessageMentions(event.appended[j])
if (event.appended[j].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event.appended[j])
}
}
newMessagesFiltered.push(event)
}
// this.messagesSet.add(newMessages[i].id)
// newMessages[i].text = await this.getPlaintext(newMessages[i])
// this.interpolateMessageMentions(newMessages[i])
// if (newMessages[i].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(newMessages[i])
// if (newMessages[i].appended) {
// for (let j = 0; j < newMessages[i].appended.length; j++) {
// this.messagesSet.add(newMessages[i].appended[j].id)
// newMessages[i].appended[j].text = await this.getPlaintext(newMessages[i].appended[j])
// this.interpolateMessageMentions(newMessages[i].appended[j])
// if (newMessages[i].appended[j].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(newMessages[i].appended[j])
// }
// }
// }
if (newMessages.length === 0) {
this.canLoadMore = false
}
this.messages = newMessages.concat(this.messages)
// this.messages = newMessages.concat(this.messages)
this.messages = newMessagesFiltered.concat(this.messages)
done(!this.canLoadMore)
},
@ -354,7 +341,45 @@ export default {
}, datestamps[0].innerText)
// console.log(messageScroll.scrollHeight, messageScroll.clientHeight, messageScroll.scrollTop)
// console.log('currentDatestamp', this.currentDatestamp)
}
},
async addMessage(event) {
if (this.messagesSet.has(event.id)) return
this.messagesSet.add(event.id)
await this.lock()
event.text = await this.getPlaintext(event)
this.unlock()
this.interpolateMessageMentions(event)
if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
let messageScroll = this.$refs.messageScroll
let scrollToBottom = 100 > Math.abs((messageScroll.scrollHeight - messageScroll.clientHeight) - messageScroll.scrollTop) ||
messageScroll.scrollHeight === messageScroll.clientHeight
if (this.messages.length === 0) {
this.messages.push(event)
} else {
let last = this.messages[this.messages.length - 1]
if (
event.pubkey === this.$store.state.keys.pub &&
last.pubkey === event.pubkey &&
last.created_at + 120 >= event.created_at
) {
last.appended = last.appended || []
last.appended.push(event)
} else {
this.messages.push(event)
}
}
if (scrollToBottom) {
this.$store.commit('haveReadMessage', this.$route.params.pubkey)
this.scrollToBottom()
} else if (event.pubkey === this.$route.params.pubkey) {
this.unreadMessagesSet.add(event.id)
}
},
}
}
</script>

View File

@ -90,9 +90,11 @@ export default {
if (loadedNotifications.length < 40) {
this.reachedEnd = true
}
loadedNotifications = loadedNotifications.filter(event => !this.notificationsSet.has(event.id))
// loadedNotifications = loadedNotifications.filter(event => !this.notificationsSet.has(event.id))
this.interpolateEventMentions(loadedNotifications)
loadedNotifications.forEach(event => {
if (this.notificationsSet.has(event.id)) return
this.notificationsSet.add(event.id)
this.addNotificationEvent(event)
this.$store.dispatch('useProfile', {pubkey: event.pubkey, request: true})

View File

@ -376,27 +376,50 @@ export async function publishContactList(store) {
var tags = event?.tags || []
// remove contacts that we're not following anymore
tags = tags.filter(
([t, v]) => t === 'p' && store.state.following.find(f => f === v)
)
// tags = tags.filter(
// ([t, v]) => t === 'p' && store.state.following.find(f => f === v)
// )
// now we merely add to the existing event because it might contain more data in the
// tags that we don't want to replace
// check existing event because it might contain more data in the
// tags that we don't want to replace, if so push existing event tag,
// else push state.following tag
let newTags = []
await Promise.all(
store.state.following.map(async pubkey => {
if (!tags.find(([t, v]) => t === 'p' && v === pubkey)) {
tags.push(await getPubKeyTagWithRelay(pubkey))
let index = tags.findIndex(([t, v]) => t === 'p' && v === pubkey)
if (index >= 0) {
newTags.push(tags[index])
} else {
newTags.push(await getPubKeyTagWithRelay(pubkey))
}
})
)
// now we merely add to the existing event because it might contain more data in the
// tags that we don't want to replace
// await Promise.all(
// store.state.following.map(async pubkey => {
// if (!tags.find(([t, v]) => t === 'p' && v === pubkey)) {
// tags.push(await getPubKeyTagWithRelay(pubkey))
// }
// })
// )
// event = {
// pubkey: store.state.keys.pub,
// created_at: Math.floor(Date.now() / 1000),
// kind: 3,
// tags,
// newTags,
// content: JSON.stringify(store.state.relays)
// }
event = await pool.publish({
pubkey: store.state.keys.pub,
created_at: Math.floor(Date.now() / 1000),
kind: 3,
tags,
tags: newTags,
content: JSON.stringify(store.state.relays)
})
console.log(event)
await store.dispatch('addEvent', {event})

View File

@ -9,6 +9,7 @@ export default function (store) {
case 'setRelayOpt':
case 'follow':
case 'unfollow':
case 'reorderFollows':
// make an event kind3 and publish it
store.dispatch('publishContactList')
break

View File

@ -66,6 +66,10 @@ export function unfollow(state, key) {
if (idx >= 0) state.following.splice(idx, 1)
}
export function reorderFollows(state, following) {
state.following = following
}
export function addProfileToCache(
state,
{pubkey, name, about, picture, nip05}

View File

@ -66,7 +66,7 @@ export async function processMentions(event) {
export async function extractMentions(text, tags) {
// const mentionRegex = /\B@(?<p>[a-f0-9]{64})\b/g
// const mentionRegex = /@((?<t>[a-z]{1}):{1})?(?<p>[a-f0-9]{64})\b/g
const mentionRegex = /(?<t>[@&]{1})(?<p>[a-f0-9]{64})/g
const mentionRegex = /(?<t>[@&]{1})(?<p>[a-f0-9]{64})\b/g
let tagIndexMap = {}
// event.tags.filter(([t, v]) => (t === 'p' || t === 'e') && v).forEach(([t, v], index) => tagIndexMap[v] = index)

View File

@ -144,7 +144,7 @@ export default {
menuItemTemplate: item => {
return `
<div class="flex row no-wrap items-center" style="gap: .2rem;">
<div class="flex row no-wrap items-center" style="gap: .2rem; width: 100%;">
<div style="border-radius: 10px">
<img src=${this.$store.getters.avatar(item.original.value.pubkey)} style="object-fit: cover; height: 1.5rem; width: 1.5rem;"/>
</div>

View File

@ -350,7 +350,7 @@ const methods = {
startkey: [ourPubKey, {}],
endkey: [ourPubKey, since]
})
return result.rows.length
return result.rows.filter((v, i, a) => a.indexOf(v) === i).length
},
async dbGetUnreadMessages(pubkey, since) {

View File

@ -6141,6 +6141,11 @@ sockjs@^0.3.21:
uuid "^8.3.2"
websocket-driver "^0.7.4"
sortablejs@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@ -6785,6 +6790,13 @@ vue@^3.0.0:
"@vue/server-renderer" "3.2.36"
"@vue/shared" "3.2.36"
vuedraggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
dependencies:
sortablejs "1.14.0"
vuex@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.2.tgz#f896dbd5bf2a0e963f00c67e9b610de749ccacc9"