mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 13:33:22 +00:00
tweaked mentions, feed, following
This commit is contained in:
parent
59a77368c6
commit
ec3d3396d4
@ -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": {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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;
|
||||
|
@ -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')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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})
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
12
yarn.lock
12
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user