mirror of
https://github.com/styppo/hamstr.git
synced 2024-09-16 15:03:30 +00:00
feat(i18n): add spanish translations
This commit is contained in:
parent
6513463cd2
commit
b389d3feb9
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@ -3,13 +3,6 @@
|
|||||||
"editor.guides.bracketPairs": true,
|
"editor.guides.bracketPairs": true,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.codeActionsOnSave": [
|
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
|
||||||
"source.fixAll.eslint"
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"]
|
||||||
],
|
}
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"vue"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"bech32-buffer": "^0.2.1",
|
"bech32-buffer": "^0.2.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"cross-fetch": "^3.1.5",
|
"cross-fetch": "^3.1.5",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"emoji-mart-vue-fast": "^12.0.1",
|
"emoji-mart-vue-fast": "^12.0.1",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
"light-bolt11-decoder": "^2.1.0",
|
"light-bolt11-decoder": "^2.1.0",
|
||||||
@ -23,7 +24,6 @@
|
|||||||
"markdown-it-emoji": "^2.0.2",
|
"markdown-it-emoji": "^2.0.2",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
"moment": "^2.29.4",
|
|
||||||
"nostr-tools": "^1.1.1",
|
"nostr-tools": "^1.1.1",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.11",
|
||||||
"pinia-plugin-persistedstate": "^3.0.2",
|
"pinia-plugin-persistedstate": "^3.0.2",
|
||||||
|
@ -2,13 +2,20 @@ import { boot } from 'quasar/wrappers'
|
|||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import messages from 'src/i18n'
|
import messages from 'src/i18n'
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
const [lng = 'en'] = (navigator?.language || '').split('-')
|
||||||
const i18n = createI18n({
|
|
||||||
locale: 'en-US',
|
|
||||||
globalInjection: true,
|
|
||||||
messages
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: lng,
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
globalInjection: true,
|
||||||
|
messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default boot(({ app }) => {
|
||||||
// Set i18n instance on app
|
// Set i18n instance on app
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const $t = i18n.global.t
|
||||||
|
|
||||||
|
export { $t }
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div ref="button" class="async-load-button">
|
<div ref="button" class="async-load-button">
|
||||||
<q-btn
|
<q-btn
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:label="noMore ? labelNoMore : label"
|
:label="$t(noMore ? labelNoMore : label)"
|
||||||
@click="load"
|
@click="load"
|
||||||
size="md"
|
size="md"
|
||||||
flat
|
flat
|
||||||
@ -23,16 +23,16 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Load more'
|
default: 'Load more',
|
||||||
},
|
},
|
||||||
labelNoMore: {
|
labelNoMore: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'No more items. Try again?'
|
default: 'No more items. Try again?',
|
||||||
},
|
},
|
||||||
autoload: {
|
autoload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -53,7 +53,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$emit('loaded', result)
|
this.$emit('loaded', result)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.autoload) {
|
if (this.autoload) {
|
||||||
@ -66,7 +66,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (this.observer) this.observer.disconnect()
|
if (this.observer) this.observer.disconnect()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
<div ref="link" class="async-load-link" @click="load">
|
<div ref="link" class="async-load-link" @click="load">
|
||||||
<q-spinner v-if="loading" size="sm" />
|
<q-spinner v-if="loading" size="sm" />
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ noMore ? prefixNoMore : (!hasItems ? prefix : '') }}
|
{{ $t(noMore ? prefixNoMore : !hasItems ? prefix : "") }}
|
||||||
<a>
|
<a>
|
||||||
{{ noMore ? labelNoMore : label }}
|
{{ $t(noMore ? labelNoMore : label) }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -23,19 +23,19 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Load more'
|
default: 'Load more',
|
||||||
},
|
},
|
||||||
labelNoMore: {
|
labelNoMore: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Try again?'
|
default: 'Try again?',
|
||||||
},
|
},
|
||||||
prefix: {
|
prefix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Nothing here.'
|
default: 'Nothing here.',
|
||||||
},
|
},
|
||||||
prefixNoMore: {
|
prefixNoMore: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Nothing found.'
|
default: 'Nothing found.',
|
||||||
},
|
},
|
||||||
hasItems: {
|
hasItems: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -44,7 +44,7 @@ export default defineComponent({
|
|||||||
autoload: {
|
autoload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -65,7 +65,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$emit('loaded', result)
|
this.$emit('loaded', result)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.autoload) {
|
if (this.autoload) {
|
||||||
@ -78,7 +78,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (this.observer) this.observer.disconnect()
|
if (this.observer) this.observer.disconnect()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="text"
|
v-model="text"
|
||||||
:placeholder="placeholder"
|
:placeholder="$t(placeholder)"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
@input="resize"
|
@input="resize"
|
||||||
@ -30,7 +30,7 @@ export default {
|
|||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'What\'s happening?',
|
default: "What's happening?",
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -43,7 +43,7 @@ export default {
|
|||||||
submitOnEnter: {
|
submitOnEnter: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'submit'],
|
emits: ['update:modelValue', 'submit'],
|
||||||
data() {
|
data() {
|
||||||
@ -78,7 +78,11 @@ export default {
|
|||||||
},
|
},
|
||||||
insertText(text) {
|
insertText(text) {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
textarea.setRangeText(text, textarea.selectionStart, textarea.selectionEnd)
|
textarea.setRangeText(
|
||||||
|
text,
|
||||||
|
textarea.selectionStart,
|
||||||
|
textarea.selectionEnd
|
||||||
|
)
|
||||||
|
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
if (textarea.selectionStart === textarea.selectionEnd) {
|
if (textarea.selectionStart === textarea.selectionEnd) {
|
||||||
@ -107,10 +111,9 @@ export default {
|
|||||||
if (this.text) {
|
if (this.text) {
|
||||||
this.resize()
|
this.resize()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -18,19 +18,19 @@ import 'emoji-mart-vue-fast/css/emoji-mart.css'
|
|||||||
export default {
|
export default {
|
||||||
name: 'EmojiPicker',
|
name: 'EmojiPicker',
|
||||||
components: {
|
components: {
|
||||||
Picker
|
Picker,
|
||||||
},
|
},
|
||||||
emits: ['select'],
|
emits: ['select'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
emojiIndex: new EmojiIndex(emojiData)
|
emojiIndex: new EmojiIndex(emojiData),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSelect(emoji) {
|
onSelect(emoji) {
|
||||||
this.$emit('select', emoji)
|
this.$emit('select', emoji)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="post-editor" :class="{ compact, collapsed, connector }">
|
||||||
class="post-editor"
|
|
||||||
:class="{compact, collapsed, connector}"
|
|
||||||
>
|
|
||||||
<div class="post-editor-author">
|
<div class="post-editor-author">
|
||||||
<div v-if="connector" class="connector-top">
|
<div v-if="connector" class="connector-top">
|
||||||
<div class="connector-line"></div>
|
<div class="connector-line"></div>
|
||||||
@ -24,7 +21,7 @@
|
|||||||
<div class="controls-media-item">
|
<div class="controls-media-item">
|
||||||
<BaseIcon icon="emoji" />
|
<BaseIcon icon="emoji" />
|
||||||
<q-menu ref="menuEmojiPicker">
|
<q-menu ref="menuEmojiPicker">
|
||||||
<EmojiPicker @select="onEmojiSelected"/>
|
<EmojiPicker @select="onEmojiSelected" />
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-media-item disabled">
|
<div class="controls-media-item disabled">
|
||||||
@ -32,15 +29,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-submit">
|
<div class="controls-submit">
|
||||||
<button :disabled="!hasContent() || publishing" @click="publishPost" class="btn btn-primary btn-sm">
|
<button
|
||||||
|
:disabled="!hasContent() || publishing"
|
||||||
|
@click="publishPost"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
>
|
||||||
<q-spinner v-if="publishing" />
|
<q-spinner v-if="publishing" />
|
||||||
<span v-else>Post</span>
|
<span v-else>{{ $t("Post") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-editor-fake-submit" v-if="collapsed">
|
<div class="post-editor-fake-submit" v-if="collapsed">
|
||||||
<button class="btn btn-primary btn-sm" disabled>Post</button>
|
<button class="btn btn-primary btn-sm" disabled>{{ $t("Post") }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -50,9 +51,10 @@ import AutoSizeTextarea from 'components/CreatePost/AutoSizeTextarea.vue'
|
|||||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||||
import BaseIcon from 'components/BaseIcon/index.vue'
|
import BaseIcon from 'components/BaseIcon/index.vue'
|
||||||
import EmojiPicker from 'components/CreatePost/EmojiPicker.vue'
|
import EmojiPicker from 'components/CreatePost/EmojiPicker.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import EventBuilder from 'src/nostr/EventBuilder'
|
import EventBuilder from 'src/nostr/EventBuilder'
|
||||||
|
import { $t } from 'src/boot/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PostEditor',
|
name: 'PostEditor',
|
||||||
@ -69,7 +71,7 @@ export default {
|
|||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'What\'s happening?',
|
default: "What's happening?",
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -82,7 +84,7 @@ export default {
|
|||||||
expanded: {
|
expanded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ['publish'],
|
emits: ['publish'],
|
||||||
data() {
|
data() {
|
||||||
@ -116,25 +118,30 @@ export default {
|
|||||||
this.publishing = true
|
this.publishing = true
|
||||||
|
|
||||||
const event = this.ancestor
|
const event = this.ancestor
|
||||||
? EventBuilder.reply(this.ancestor, this.app.myPubkey, this.content).build()
|
? EventBuilder.reply(
|
||||||
|
this.ancestor,
|
||||||
|
this.app.myPubkey,
|
||||||
|
this.content
|
||||||
|
).build()
|
||||||
: EventBuilder.post(this.app.myPubkey, this.content).build()
|
: EventBuilder.post(this.app.myPubkey, this.content).build()
|
||||||
if (!await this.app.signEvent(event)) return
|
if (!(await this.app.signEvent(event))) return
|
||||||
|
|
||||||
const numRelays = await this.nostr.publish(event)
|
const numRelays = await this.nostr.publish(event)
|
||||||
if (numRelays) {
|
if (numRelays) {
|
||||||
this.reset()
|
this.reset()
|
||||||
this.$emit('publish', event)
|
this.$emit('publish', event)
|
||||||
|
|
||||||
// TODO i18n
|
|
||||||
const postType = this.ancestor ? 'Reply' : 'Post'
|
const postType = this.ancestor ? 'Reply' : 'Post'
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: `${postType} published to ${numRelays} relays`,
|
message: $t(`${postType} published to {numRelays} relays`, {
|
||||||
|
numRelays,
|
||||||
|
}),
|
||||||
color: 'positive',
|
color: 'positive',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: `Failed to publish post`,
|
message: $t(`Failed to publish post`),
|
||||||
color: 'negative'
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,11 +211,11 @@ export default {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
fill: $color-primary
|
fill: $color-primary;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="feed">
|
<div class="feed">
|
||||||
<div class="load-more-container" :class="{'more-available': numUnreads}">
|
<div class="load-more-container" :class="{ 'more-available': numUnreads }">
|
||||||
<AsyncLoadButton
|
<AsyncLoadButton
|
||||||
v-if="numUnreads"
|
v-if="numUnreads"
|
||||||
:load-fn="loadNewer"
|
:load-fn="loadNewer"
|
||||||
:label="`Load ${numUnreads} unread`"
|
:label="$t('Load {unread} unread', { unread: numUnreads })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Thread v-for="thread in visible" :key="thread[0].id" :thread="thread" class="full-width" />
|
<Thread
|
||||||
|
v-for="thread in visible"
|
||||||
|
:key="thread[0].id"
|
||||||
|
:thread="thread"
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
|
||||||
<ListPlaceholder :count="visible.length" :loading="loading" />
|
<ListPlaceholder :count="visible.length" :loading="loading" />
|
||||||
|
|
||||||
<AsyncLoadButton
|
<AsyncLoadButton v-if="visible.length" :load-fn="loadOlder" autoload />
|
||||||
v-if="visible.length"
|
|
||||||
:load-fn="loadOlder"
|
|
||||||
autoload
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -24,8 +25,8 @@
|
|||||||
import AsyncLoadButton from 'components/AsyncLoadButton.vue'
|
import AsyncLoadButton from 'components/AsyncLoadButton.vue'
|
||||||
import Thread from 'components/Post/Thread.vue'
|
import Thread from 'components/Post/Thread.vue'
|
||||||
import ListPlaceholder from 'components/ListPlaceholder.vue'
|
import ListPlaceholder from 'components/ListPlaceholder.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import DateUtils from 'src/utils/DateUtils'
|
import DateUtils from 'src/utils/DateUtils'
|
||||||
import Bots from 'src/utils/bots'
|
import Bots from 'src/utils/bots'
|
||||||
|
|
||||||
@ -39,13 +40,13 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
ListPlaceholder,
|
ListPlaceholder,
|
||||||
Thread,
|
Thread,
|
||||||
AsyncLoadButton
|
AsyncLoadButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
feed: {
|
feed: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -73,30 +74,31 @@ export default {
|
|||||||
},
|
},
|
||||||
timestampOldest() {
|
timestampOldest() {
|
||||||
return this.visible[this.visible.length - 1]?.[0]?.createdAt
|
return this.visible[this.visible.length - 1]?.[0]?.createdAt
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
const filters = typeof this.feed.filters === 'function'
|
const filters =
|
||||||
? this.feed.filters()
|
typeof this.feed.filters === 'function'
|
||||||
: this.feed.filters
|
? this.feed.filters()
|
||||||
this.stream = this.nostr.stream(
|
: this.feed.filters
|
||||||
filters,
|
this.stream = this.nostr.stream(filters, {
|
||||||
{
|
subId: `feed:${this.feed.name}`,
|
||||||
subId: `feed:${this.feed.name}`,
|
timeout: 3000,
|
||||||
timeout: 3000,
|
})
|
||||||
}
|
this.stream.on('init', (notes) => {
|
||||||
)
|
const data =
|
||||||
this.stream.on('init', notes => {
|
typeof this.feed.data === 'function'
|
||||||
const data = typeof this.feed.data === 'function'
|
? this.feed.data()
|
||||||
? this.feed.data()
|
: this.feed.data || []
|
||||||
: this.feed.data || []
|
|
||||||
const items = notes
|
const items = notes
|
||||||
.concat(data)
|
.concat(data)
|
||||||
.filter(note => this.filterNote(note, this.feed.hideBots))
|
.filter((note) => this.filterNote(note, this.feed.hideBots))
|
||||||
.map(note => [note]) // TODO Single element thread
|
.map((note) => [note]) // TODO Single element thread
|
||||||
.sort(feedOrder)
|
.sort(feedOrder)
|
||||||
.filter((item, pos, array) => !pos || item[0].id !== array[pos - 1][0].id)
|
.filter(
|
||||||
|
(item, pos, array) => !pos || item[0].id !== array[pos - 1][0].id
|
||||||
|
)
|
||||||
|
|
||||||
this.visible = items.slice(0, MAX_ITEMS_VISIBLE)
|
this.visible = items.slice(0, MAX_ITEMS_VISIBLE)
|
||||||
this.loading = false
|
this.loading = false
|
||||||
@ -104,14 +106,16 @@ export default {
|
|||||||
this.$emit('load', this.feed)
|
this.$emit('load', this.feed)
|
||||||
|
|
||||||
// Wait a bit before showing the first unreads
|
// Wait a bit before showing the first unreads
|
||||||
setTimeout(() => this.recentlyLoaded = false, 5000)
|
setTimeout(() => (this.recentlyLoaded = false), 5000)
|
||||||
})
|
})
|
||||||
this.stream.on('update', note => {
|
this.stream.on('update', (note) => {
|
||||||
if (!this.filterNote(note, this.feed.hideBots)) return
|
if (!this.filterNote(note, this.feed.hideBots)) return
|
||||||
if (note.createdAt >= this.timestampNewest) {
|
if (note.createdAt >= this.timestampNewest) {
|
||||||
this.newer.push([note]) // TODO Single element thread
|
this.newer.push([note]) // TODO Single element thread
|
||||||
} else if (note.createdAt >= this.timestampOldest) {
|
} else if (note.createdAt >= this.timestampOldest) {
|
||||||
const idx = this.visible.findIndex(thread => thread[0].createdAt >= note.createdAt)
|
const idx = this.visible.findIndex(
|
||||||
|
(thread) => thread[0].createdAt >= note.createdAt
|
||||||
|
)
|
||||||
this.visible.splice(idx, 0, [note]) // TODO Single element thread
|
this.visible.splice(idx, 0, [note]) // TODO Single element thread
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -138,17 +142,18 @@ export default {
|
|||||||
|
|
||||||
// Wait a bit before showing unreads again
|
// Wait a bit before showing unreads again
|
||||||
this.recentlyLoaded = true
|
this.recentlyLoaded = true
|
||||||
setTimeout(() => this.recentlyLoaded = false, 5000)
|
setTimeout(() => (this.recentlyLoaded = false), 5000)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
async loadOlder() {
|
async loadOlder() {
|
||||||
const feedFilters = typeof this.feed.filters === 'function'
|
const feedFilters =
|
||||||
? this.feed.filters()
|
typeof this.feed.filters === 'function'
|
||||||
: this.feed.filters
|
? this.feed.filters()
|
||||||
|
: this.feed.filters
|
||||||
|
|
||||||
const until = this.timestampOldest || DateUtils.now()
|
const until = this.timestampOldest || DateUtils.now()
|
||||||
const filters = Object.assign({}, feedFilters, {until})
|
const filters = Object.assign({}, feedFilters, { until })
|
||||||
|
|
||||||
if (this.older.length >= filters.limit) {
|
if (this.older.length >= filters.limit) {
|
||||||
const chunk = this.older.splice(0, filters.limit)
|
const chunk = this.older.splice(0, filters.limit)
|
||||||
@ -159,11 +164,13 @@ export default {
|
|||||||
// Remove any residual older items
|
// Remove any residual older items
|
||||||
this.older = []
|
this.older = []
|
||||||
|
|
||||||
const older = await this.nostr.fetch(filters, {subId: `feed:${this.feed.name}-older`})
|
const older = await this.nostr.fetch(filters, {
|
||||||
|
subId: `feed:${this.feed.name}-older`,
|
||||||
|
})
|
||||||
const items = older
|
const items = older
|
||||||
.filter(note => note.createdAt <= until)
|
.filter((note) => note.createdAt <= until)
|
||||||
.filter(note => this.filterNote(note, this.feed.hideBots))
|
.filter((note) => this.filterNote(note, this.feed.hideBots))
|
||||||
.map(note => [note]) // TODO Single element thread
|
.map((note) => [note]) // TODO Single element thread
|
||||||
.sort(feedOrder)
|
.sort(feedOrder)
|
||||||
|
|
||||||
// TODO Deduplicate feed items
|
// TODO Deduplicate feed items
|
||||||
@ -183,7 +190,7 @@ export default {
|
|||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (this.stream) this.stream.close()
|
if (this.stream) this.stream.close()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!count" class="list-placeholder">
|
<div v-if="!count" class="list-placeholder">
|
||||||
<q-spinner v-if="loading" size="sm" />
|
<q-spinner v-if="loading" size="sm" />
|
||||||
<p v-else>{{ label }}</p>
|
<p v-else>{{ $t(label) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -19,9 +19,9 @@ export default {
|
|||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Nothing here',
|
default: 'Nothing here.',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -8,14 +8,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="(route, i) in items" :key="i">
|
<div v-for="(route, i) in items" :key="i">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
v-if="!hideItemsRequiringSignIn || !route.signInRequired || app.isSignedIn"
|
v-if="
|
||||||
|
!hideItemsRequiringSignIn || !route.signInRequired || app.isSignedIn
|
||||||
|
"
|
||||||
:icon="route.name.toLowerCase()"
|
:icon="route.name.toLowerCase()"
|
||||||
:to="route.path"
|
:to="route.path"
|
||||||
:enabled="route.enabled !== false"
|
:enabled="route.enabled !== false"
|
||||||
:indicator="route.indicator && route.indicator()"
|
:indicator="route.indicator && route.indicator()"
|
||||||
@click="$emit('mobile-menu-close')"
|
@click="$emit('mobile-menu-close')"
|
||||||
>
|
>
|
||||||
{{ route.name }}
|
{{ $t(route.name) }}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -25,14 +27,14 @@
|
|||||||
:enabled="app.isSignedIn"
|
:enabled="app.isSignedIn"
|
||||||
@click="$emit('mobile-menu-close')"
|
@click="$emit('mobile-menu-close')"
|
||||||
>
|
>
|
||||||
Profile
|
{{ $t("Profile") }}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="settings"
|
icon="settings"
|
||||||
to="/settings"
|
to="/settings"
|
||||||
@click="$emit('mobile-menu-close')"
|
@click="$emit('mobile-menu-close')"
|
||||||
>
|
>
|
||||||
Settings
|
{{ $t("Settings") }}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -40,7 +42,7 @@
|
|||||||
class="menu-post-button"
|
class="menu-post-button"
|
||||||
@click="createPost"
|
@click="createPost"
|
||||||
>
|
>
|
||||||
<span class="label">Post</span>
|
<span class="label">{{ $t("Post") }}</span>
|
||||||
<BaseIcon class="icon" icon="pen" />
|
<BaseIcon class="icon" icon="pen" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,18 +51,15 @@
|
|||||||
<ProfilePopup v-if="app.isSignedIn" />
|
<ProfilePopup v-if="app.isSignedIn" />
|
||||||
<div v-else class="sign-in" @click="signIn">
|
<div v-else class="sign-in" @click="signIn">
|
||||||
<q-icon class="icon" name="login" size="sm" />
|
<q-icon class="icon" name="login" size="sm" />
|
||||||
<div class="label">Log in</div>
|
<div class="label">{{ $t("Log in") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="mobile-close-menu-button" @click="$emit('mobile-menu-close')">
|
||||||
class="mobile-close-menu-button"
|
|
||||||
@click="$emit('mobile-menu-close')"
|
|
||||||
>
|
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<BaseIcon icon="left" />
|
<BaseIcon icon="left" />
|
||||||
</div>
|
</div>
|
||||||
<span>Close</span>
|
<span>{{ $t("Close") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</menu>
|
</menu>
|
||||||
</template>
|
</template>
|
||||||
@ -70,9 +69,9 @@ import MenuItem from 'components/MainMenu/MenuItem.vue'
|
|||||||
import BaseIcon from 'components/BaseIcon'
|
import BaseIcon from 'components/BaseIcon'
|
||||||
import ProfilePopup from 'components/MainMenu/ProfilePopup'
|
import ProfilePopup from 'components/MainMenu/ProfilePopup'
|
||||||
import Logo from 'components/Logo.vue'
|
import Logo from 'components/Logo.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {MENU_ITEMS} from 'components/MainMenu/constants.js'
|
import { MENU_ITEMS } from 'components/MainMenu/constants.js'
|
||||||
import {hexToBech32} from 'src/utils/utils'
|
import { hexToBech32 } from 'src/utils/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainMenu',
|
name: 'MainMenu',
|
||||||
@ -80,13 +79,13 @@ export default {
|
|||||||
Logo,
|
Logo,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
ProfilePopup
|
ProfilePopup,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
hideItemsRequiringSignIn: {
|
hideItemsRequiringSignIn: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ['mobile-menu-close'],
|
emits: ['mobile-menu-close'],
|
||||||
data() {
|
data() {
|
||||||
@ -108,8 +107,8 @@ export default {
|
|||||||
this.$emit('mobile-menu-close')
|
this.$emit('mobile-menu-close')
|
||||||
this.app.signIn()
|
this.app.signIn()
|
||||||
},
|
},
|
||||||
hexToBech32
|
hexToBech32,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -134,7 +133,8 @@ menu {
|
|||||||
}
|
}
|
||||||
&-logo {
|
&-logo {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
svg, img {
|
svg,
|
||||||
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<base-icon :icon="item.icon" />
|
<base-icon :icon="item.icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ item.name }}
|
{{ $t(item.name) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,11 +15,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-menu :offset="[0, 20]" target=".menu-profile" class="menu-profile-popup" >
|
<q-menu :offset="[0, 20]" target=".menu-profile" class="menu-profile-popup">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="(_, pk) in settings.accounts" :key="pk" class="popup-header" @click="app.switchAccount(pk)" v-close-popup>
|
<div
|
||||||
|
v-for="(_, pk) in settings.accounts"
|
||||||
|
:key="pk"
|
||||||
|
class="popup-header"
|
||||||
|
@click="app.switchAccount(pk)"
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
<div class="sidebar-profile-pic">
|
<div class="sidebar-profile-pic">
|
||||||
<UserAvatar :pubkey="pk" :clickable="false"/>
|
<UserAvatar :pubkey="pk" :clickable="false" />
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-profile-items">
|
<div class="menu-profile-items">
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
@ -32,18 +38,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="popup-spacing">
|
<hr class="popup-spacing" />
|
||||||
<div class="popup-body">
|
<div class="popup-body">
|
||||||
<div class="popup-body-item" @click="app.signIn()" v-close-popup>
|
<div class="popup-body-item" @click="app.signIn()" v-close-popup>
|
||||||
<p>Add an account</p>
|
<p>{{ $t("Add an account") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr class="popup-spacing">
|
<hr class="popup-spacing" />
|
||||||
<div
|
<div
|
||||||
class="popup-body-item"
|
class="popup-body-item"
|
||||||
@click="$refs.logout.show()"
|
@click="$refs.logout.show()"
|
||||||
v-close-popup
|
v-close-popup
|
||||||
>
|
>
|
||||||
<p>Logout from <span><UserName :pubkey="pubkey" /></span></p>
|
<p>
|
||||||
|
{{ $t("Logout from") }}
|
||||||
|
<span><UserName :pubkey="pubkey" /></span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -57,8 +66,8 @@ import BaseIcon from 'components/BaseIcon/index.vue'
|
|||||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
import LogoutDialog from 'components/User/LogoutDialog.vue'
|
import LogoutDialog from 'components/User/LogoutDialog.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProfilePopup',
|
name: 'ProfilePopup',
|
||||||
@ -66,7 +75,7 @@ export default {
|
|||||||
LogoutDialog,
|
LogoutDialog,
|
||||||
UserName,
|
UserName,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
BaseIcon
|
BaseIcon,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -80,7 +89,7 @@ export default {
|
|||||||
},
|
},
|
||||||
accounts() {
|
accounts() {
|
||||||
return this.settings.accounts
|
return this.settings.accounts
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -94,7 +103,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
padding: .5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
transition: 120ms ease-in-out;
|
transition: 120ms ease-in-out;
|
||||||
@ -207,10 +216,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: $phone) {
|
@media screen and (max-width: $phone) {
|
||||||
.menu-profile {
|
.menu-profile {
|
||||||
padding: .5rem;
|
padding: 0.5rem;
|
||||||
margin: 0 auto 1rem auto;
|
margin: 0 auto 1rem auto;
|
||||||
&-wrapper {
|
&-wrapper {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<PostRenderer v-if="note" :note="note" />
|
<PostRenderer v-if="note" :note="note" />
|
||||||
<span v-else-if="!decryptFailed" class="click-to-decrypt" @click="clickToDecrypt && decrypt()">Click to decrypt</span>
|
<span
|
||||||
<span v-else class="decrypt-failed" @click="decrypt">Decryption failed</span>
|
v-else-if="!decryptFailed"
|
||||||
|
class="click-to-decrypt"
|
||||||
|
@click="clickToDecrypt && decrypt()"
|
||||||
|
>
|
||||||
|
{{ $t("Click to decrypt") }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="decrypt-failed" @click="decrypt">
|
||||||
|
{{ $t("Decryption failed") }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||||
import Note from 'src/nostr/model/Note'
|
import Note from 'src/nostr/model/Note'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EncryptedMessage',
|
name: 'EncryptedMessage',
|
||||||
components: {PostRenderer},
|
components: { PostRenderer },
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -20,7 +28,7 @@ export default {
|
|||||||
clickToDecrypt: {
|
clickToDecrypt: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -38,17 +46,21 @@ export default {
|
|||||||
const note = new Note(this.message.id, this.message)
|
const note = new Note(this.message.id, this.message)
|
||||||
note.content = this.message.plaintext
|
note.content = this.message.plaintext
|
||||||
return note
|
return note
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async decrypt() {
|
async decrypt() {
|
||||||
if (this.message.plaintext) return
|
if (this.message.plaintext) return
|
||||||
try {
|
try {
|
||||||
const messageId = this.message.id
|
const messageId = this.message.id
|
||||||
const counterparty = this.message.author === this.app.myPubkey
|
const counterparty =
|
||||||
? this.message.recipient
|
this.message.author === this.app.myPubkey
|
||||||
: this.message.author
|
? this.message.recipient
|
||||||
const plaintext = await this.app.decryptMessage(counterparty, this.message.content)
|
: this.message.author
|
||||||
|
const plaintext = await this.app.decryptMessage(
|
||||||
|
counterparty,
|
||||||
|
this.message.content
|
||||||
|
)
|
||||||
// The message can change while we are decrypting it, so we need to make sure not to cache the wrong message.
|
// The message can change while we are decrypting it, so we need to make sure not to cache the wrong message.
|
||||||
if (this.message.id === messageId) {
|
if (this.message.id === messageId) {
|
||||||
this.message.cachePlaintext(plaintext)
|
this.message.cachePlaintext(plaintext)
|
||||||
@ -57,7 +69,7 @@ export default {
|
|||||||
console.error('Failed to decrypt message', e)
|
console.error('Failed to decrypt message', e)
|
||||||
this.decryptFailed = true
|
this.decryptFailed = true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (this.app.activeAccount.canDecrypt()) {
|
if (this.app.activeAccount.canDecrypt()) {
|
||||||
@ -69,8 +81,8 @@ export default {
|
|||||||
if (this.app.activeAccount.canDecrypt()) {
|
if (this.app.activeAccount.canDecrypt()) {
|
||||||
await this.decrypt()
|
await this.decrypt()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<div class="inline-controls-item">
|
<div class="inline-controls-item">
|
||||||
<BaseIcon icon="emoji" />
|
<BaseIcon icon="emoji" />
|
||||||
<q-menu ref="menuEmojiPicker">
|
<q-menu ref="menuEmojiPicker">
|
||||||
<EmojiPicker @select="onEmojiSelected"/>
|
<EmojiPicker @select="onEmojiSelected" />
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,9 +38,10 @@
|
|||||||
import BaseIcon from 'components/BaseIcon/index.vue'
|
import BaseIcon from 'components/BaseIcon/index.vue'
|
||||||
import EmojiPicker from 'components/CreatePost/EmojiPicker.vue'
|
import EmojiPicker from 'components/CreatePost/EmojiPicker.vue'
|
||||||
import AutoSizeTextarea from 'components/CreatePost/AutoSizeTextarea.vue'
|
import AutoSizeTextarea from 'components/CreatePost/AutoSizeTextarea.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import EventBuilder from 'src/nostr/EventBuilder'
|
import EventBuilder from 'src/nostr/EventBuilder'
|
||||||
|
import { $t } from 'src/boot/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MessageEditor',
|
name: 'MessageEditor',
|
||||||
@ -61,7 +62,7 @@ export default {
|
|||||||
autofocus: {
|
autofocus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ['publish'],
|
emits: ['publish'],
|
||||||
data() {
|
data() {
|
||||||
@ -95,10 +96,17 @@ export default {
|
|||||||
async publishMessage() {
|
async publishMessage() {
|
||||||
this.publishing = true
|
this.publishing = true
|
||||||
|
|
||||||
const ciphertext = await this.app.encryptMessage(this.recipient, this.content)
|
const ciphertext = await this.app.encryptMessage(
|
||||||
|
this.recipient,
|
||||||
|
this.content
|
||||||
|
)
|
||||||
if (!ciphertext) return
|
if (!ciphertext) return
|
||||||
const event = EventBuilder.message(this.app.myPubkey, this.recipient, ciphertext).build()
|
const event = EventBuilder.message(
|
||||||
if (!await this.app.signEvent(event)) return
|
this.app.myPubkey,
|
||||||
|
this.recipient,
|
||||||
|
ciphertext
|
||||||
|
).build()
|
||||||
|
if (!(await this.app.signEvent(event))) return
|
||||||
|
|
||||||
if (await this.nostr.publish(event)) {
|
if (await this.nostr.publish(event)) {
|
||||||
this.reset()
|
this.reset()
|
||||||
@ -106,8 +114,8 @@ export default {
|
|||||||
this.$emit('publish', event)
|
this.$emit('publish', event)
|
||||||
} else {
|
} else {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: `Failed to send message`,
|
message: $t(`Failed to send message`),
|
||||||
color: 'negative'
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +126,7 @@ export default {
|
|||||||
if (this.autofocus) {
|
if (this.autofocus) {
|
||||||
this.$nextTick(this.focus.bind(this))
|
this.$nextTick(this.focus.bind(this))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -136,7 +144,7 @@ export default {
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 12px 36px 12px 1rem;
|
padding: 12px 36px 12px 1rem;
|
||||||
margin-right: .5rem;
|
margin-right: 0.5rem;
|
||||||
textarea {
|
textarea {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -164,11 +172,11 @@ export default {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
fill: $color-primary
|
fill: $color-primary;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-header" :class="{dense}">
|
<div class="page-header" :class="{ dense }">
|
||||||
<div
|
<div v-if="backButton" class="back-button" @click="$router.go(-1)">
|
||||||
v-if="backButton"
|
|
||||||
class="back-button"
|
|
||||||
@click="$router.go(-1)"
|
|
||||||
>
|
|
||||||
<base-icon icon="back" />
|
<base-icon icon="back" />
|
||||||
</div>
|
</div>
|
||||||
<div :class="{'profile-info': !!subline}">
|
<div :class="{ 'profile-info': !!subline }">
|
||||||
<slot>
|
<slot>
|
||||||
<h2>{{ title || titleFromRoute() || 'Home' }}</h2>
|
<h2>{{ $t(title || titleFromRoute() || "Home") }}</h2>
|
||||||
<span v-if="subline">{{ subline }}</span>
|
<span v-if="subline">{{ subline }}</span>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
@ -31,7 +27,7 @@ export default defineComponent({
|
|||||||
name: 'PageHeader',
|
name: 'PageHeader',
|
||||||
components: {
|
components: {
|
||||||
Logo,
|
Logo,
|
||||||
BaseIcon
|
BaseIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
@ -53,14 +49,14 @@ export default defineComponent({
|
|||||||
dense: {
|
dense: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
titleFromRoute() {
|
titleFromRoute() {
|
||||||
const route = this.$route.name?.toLowerCase()
|
const route = this.$route.name?.toLowerCase()
|
||||||
return route?.charAt(0).toUpperCase() + route?.substring(1)
|
return route?.charAt(0).toUpperCase() + route?.substring(1)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -116,14 +112,14 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
&.dense {
|
&.dense {
|
||||||
.back-button {
|
.back-button {
|
||||||
margin-right: .5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $phone) {
|
@media screen and (max-width: $phone) {
|
||||||
.page-header {
|
.page-header {
|
||||||
padding: .4rem 1rem;
|
padding: 0.4rem 1rem;
|
||||||
.logo {
|
.logo {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="post-content-header">
|
<div class="post-content-header">
|
||||||
<p v-if="note.hasAncestor()" class="in-reply-to">
|
<p v-if="note.hasAncestor()" class="in-reply-to">
|
||||||
Replying to
|
{{ $t("Replying to") }}
|
||||||
<a @click.stop="goToProfile(ancestor?.author)">
|
<a @click.stop="goToProfile(ancestor?.author)">
|
||||||
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
||||||
</a>
|
</a>
|
||||||
@ -32,7 +32,11 @@
|
|||||||
<span>{{ formatDate(note.createdAt) }}</span>
|
<span>{{ formatDate(note.createdAt) }}</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="post-content-actions">
|
<div class="post-content-actions">
|
||||||
<PostActions :note="note" flavor="hero" @comment="$refs.editor.focus()" />
|
<PostActions
|
||||||
|
:note="note"
|
||||||
|
flavor="hero"
|
||||||
|
@comment="$refs.editor.focus()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,7 +45,7 @@
|
|||||||
:ancestor="note"
|
:ancestor="note"
|
||||||
ref="editor"
|
ref="editor"
|
||||||
compact
|
compact
|
||||||
placeholder="Post your reply"
|
:placeholder="$t('Post your reply')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,8 +56,8 @@ import UserName from 'components/User/UserName.vue'
|
|||||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||||
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import routerMixin from 'src/router/mixin'
|
import routerMixin from 'src/router/mixin'
|
||||||
import DateUtils from 'src/utils/DateUtils'
|
import DateUtils from 'src/utils/DateUtils'
|
||||||
import PostActions from 'components/Post/PostActions.vue'
|
import PostActions from 'components/Post/PostActions.vue'
|
||||||
@ -71,7 +75,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -94,7 +98,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
formatDate: DateUtils.formatDate,
|
formatDate: DateUtils.formatDate,
|
||||||
formatTime: DateUtils.formatTime,
|
formatTime: DateUtils.formatTime,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -169,10 +173,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $phone) {
|
@media screen and (max-width: $phone) {
|
||||||
.post{
|
.post {
|
||||||
&-content {
|
&-content {
|
||||||
&-header {
|
&-header {
|
||||||
span{
|
span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.created-at {
|
.created-at {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="post"
|
class="post"
|
||||||
:class="{clickable}"
|
:class="{ clickable }"
|
||||||
@click.stop="clickable && goToThread(note.id)"
|
@click.stop="clickable && goToThread(note.id)"
|
||||||
>
|
>
|
||||||
<div class="post-author">
|
<div class="post-author">
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<span class="created-at">{{ createdAt }}</span>
|
<span class="created-at">{{ createdAt }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="note.hasAncestor()" class="in-reply-to">
|
<p v-if="note.hasAncestor()" class="in-reply-to">
|
||||||
Replying to
|
{{ $t("Replying to") }}
|
||||||
<a @click.stop="goToProfile(ancestor?.author)">
|
<a @click.stop="goToProfile(ancestor?.author)">
|
||||||
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
||||||
</a>
|
</a>
|
||||||
@ -42,8 +42,8 @@ import UserAvatar from 'components/User/UserAvatar.vue'
|
|||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||||
import PostActions from 'components/Post/PostActions.vue'
|
import PostActions from 'components/Post/PostActions.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import routerMixin from 'src/router/mixin'
|
import routerMixin from 'src/router/mixin'
|
||||||
import DateUtils from 'src/utils/DateUtils'
|
import DateUtils from 'src/utils/DateUtils'
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
connectorTop: {
|
connectorTop: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -112,14 +112,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const updateInterval = DateUtils.now() - this.note.createdAt >= 3600 // 1h
|
const updateInterval =
|
||||||
? 3600 // 1h
|
DateUtils.now() - this.note.createdAt >= 3600 // 1h
|
||||||
: 60 // 1m
|
? 3600 // 1h
|
||||||
this.refreshTimer = setInterval(() => this.refreshCounter++, updateInterval * 1000)
|
: 60 // 1m
|
||||||
|
this.refreshTimer = setInterval(
|
||||||
|
() => this.refreshCounter++,
|
||||||
|
updateInterval * 1000
|
||||||
|
)
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
clearInterval(this.refreshTimer)
|
clearInterval(this.refreshTimer)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -161,7 +165,7 @@ export default {
|
|||||||
}
|
}
|
||||||
&-content {
|
&-content {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
padding: 1rem 0 .4rem;
|
padding: 1rem 0 0.4rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-width: 570px;
|
max-width: 570px;
|
||||||
&-header {
|
&-header {
|
||||||
@ -210,7 +214,7 @@ export default {
|
|||||||
}
|
}
|
||||||
&-body {
|
&-body {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
&-actions {
|
&-actions {
|
||||||
}
|
}
|
||||||
@ -222,7 +226,7 @@ export default {
|
|||||||
&-content {
|
&-content {
|
||||||
max-width: calc(100% - 48px - 1rem);
|
max-width: calc(100% - 48px - 1rem);
|
||||||
&-body {
|
&-body {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,30 @@
|
|||||||
<div class="post-actions" :class="flavor">
|
<div class="post-actions" :class="flavor">
|
||||||
<div class="action-item comment" @click.stop="comment">
|
<div class="action-item comment" @click.stop="comment">
|
||||||
<BaseIcon icon="comment" />
|
<BaseIcon icon="comment" />
|
||||||
<span>{{ stats.comments || '' }}</span>
|
<span>{{ stats.comments || "" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-item repost" @click.stop="repost">
|
<div class="action-item repost" @click.stop="repost">
|
||||||
<BaseIcon icon="repost" />
|
<BaseIcon icon="repost" />
|
||||||
<span>{{ stats.shares || '' }}</span>
|
<span>{{ stats.shares || "" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-item like" :class="{active: liked}" @click.stop="like">
|
<div class="action-item like" :class="{ active: liked }" @click.stop="like">
|
||||||
<BaseIcon :icon="liked ? 'like_filled' : 'like'" />
|
<BaseIcon :icon="liked ? 'like_filled' : 'like'" />
|
||||||
<span>{{ stats.reactions || '' }}</span>
|
<span>{{ stats.reactions || "" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseIcon from 'components/BaseIcon/index.vue'
|
import BaseIcon from 'components/BaseIcon/index.vue'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
import { useStatStore } from 'src/nostr/store/StatStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import EventBuilder from 'src/nostr/EventBuilder'
|
import EventBuilder from 'src/nostr/EventBuilder'
|
||||||
|
import { $t } from 'src/boot/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PostActions',
|
name: 'PostActions',
|
||||||
components: {BaseIcon},
|
components: { BaseIcon },
|
||||||
emits: ['comment', 'repost'],
|
emits: ['comment', 'repost'],
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
@ -34,13 +35,13 @@ export default {
|
|||||||
flavor: {
|
flavor: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'list',
|
default: 'list',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
app: useAppStore(),
|
app: useAppStore(),
|
||||||
stat: useStatStore(),
|
stat: useStatStore(),
|
||||||
nostr: useNostrStore()
|
nostr: useNostrStore(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -57,7 +58,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
comment() {
|
comment() {
|
||||||
if (this.flavor === 'list') {
|
if (this.flavor === 'list') {
|
||||||
this.app.createPost({ancestor: this.note.id})
|
this.app.createPost({ ancestor: this.note.id })
|
||||||
} else {
|
} else {
|
||||||
this.$emit('comment')
|
this.$emit('comment')
|
||||||
}
|
}
|
||||||
@ -75,21 +76,21 @@ export default {
|
|||||||
},
|
},
|
||||||
async publishLike() {
|
async publishLike() {
|
||||||
const event = EventBuilder.reaction(this.note, this.app.myPubkey).build()
|
const event = EventBuilder.reaction(this.note, this.app.myPubkey).build()
|
||||||
if (!await this.app.signEvent(event)) return
|
if (!(await this.app.signEvent(event))) return
|
||||||
if (!await this.nostr.publish(event)) {
|
if (!(await this.nostr.publish(event))) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Failed to publish reaction',
|
message: $t('Failed to publish reaction'),
|
||||||
color: 'negative',
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteLike() {
|
async deleteLike() {
|
||||||
const ids = this.ourReactions.map(r => r.id)
|
const ids = this.ourReactions.map((r) => r.id)
|
||||||
const event = EventBuilder.delete(this.app.myPubkey, ids).build()
|
const event = EventBuilder.delete(this.app.myPubkey, ids).build()
|
||||||
if (!await this.app.signEvent(event)) return
|
if (!(await this.app.signEvent(event))) return
|
||||||
if (!await this.nostr.publish(event)) {
|
if (!(await this.nostr.publish(event))) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Failed to delete reaction',
|
message: $t('Failed to delete reaction'),
|
||||||
color: 'negative',
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -109,7 +110,7 @@ export default {
|
|||||||
max-width: 490px;
|
max-width: 490px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: .5rem 0 .5rem 16px;
|
padding: 0.5rem 0 0.5rem 16px;
|
||||||
&.list {
|
&.list {
|
||||||
width: calc(100% + 9px);
|
width: calc(100% + 9px);
|
||||||
margin-left: -9px;
|
margin-left: -9px;
|
||||||
@ -140,7 +141,8 @@ export default {
|
|||||||
span {
|
span {
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
}
|
}
|
||||||
&.active, &:hover {
|
&.active,
|
||||||
|
&:hover {
|
||||||
&.comment {
|
&.comment {
|
||||||
svg {
|
svg {
|
||||||
fill: $post-action-blue;
|
fill: $post-action-blue;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative-position">
|
<div class="relative-position">
|
||||||
<div class="searchbox" :class="{focused}">
|
<div class="searchbox" :class="{ focused }">
|
||||||
<div class="searchbox-wrapper">
|
<div class="searchbox-wrapper">
|
||||||
<div class="searchbox-icon">
|
<div class="searchbox-icon">
|
||||||
<BaseIcon icon="search" />
|
<BaseIcon icon="search" />
|
||||||
@ -11,12 +11,12 @@
|
|||||||
v-model="query"
|
v-model="query"
|
||||||
ref="input"
|
ref="input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search profiles"
|
:placeholder="$t('Search profiles')"
|
||||||
@focus="toggleFocus"
|
@focus="toggleFocus"
|
||||||
@blur="toggleFocus"
|
@blur="toggleFocus"
|
||||||
@keyup="search"
|
@keyup="search"
|
||||||
@keyup.esc="$refs.input.blur()"
|
@keyup.esc="$refs.input.blur()"
|
||||||
>
|
/>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,7 +70,10 @@ export default {
|
|||||||
},
|
},
|
||||||
async search() {
|
async search() {
|
||||||
if (this.query) {
|
if (this.query) {
|
||||||
this.results = (await this.provider.queryProfiles(this.query)).slice(0, 200)
|
this.results = (await this.provider.queryProfiles(this.query)).slice(
|
||||||
|
0,
|
||||||
|
200
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
this.results = []
|
this.results = []
|
||||||
}
|
}
|
||||||
@ -80,7 +83,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import 'assets/theme/colors.scss';
|
@import "assets/theme/colors.scss";
|
||||||
|
|
||||||
.searchbox {
|
.searchbox {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -126,9 +129,9 @@ export default {
|
|||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: $color-bg;
|
background-color: $color-bg;
|
||||||
border-radius: .5rem;
|
border-radius: 0.5rem;
|
||||||
z-index: 600;
|
z-index: 600;
|
||||||
margin-top: -.75rem;
|
margin-top: -0.75rem;
|
||||||
box-shadow: $shadow-white;
|
box-shadow: $shadow-white;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
scrollbar-color: transparent transparent;
|
scrollbar-color: transparent transparent;
|
||||||
@ -136,10 +139,12 @@ export default {
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb { /* Foreground */
|
&::-webkit-scrollbar-thumb {
|
||||||
|
/* Foreground */
|
||||||
background: $color-dark-gray;
|
background: $color-dark-gray;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-track { /* Background */
|
&::-webkit-scrollbar-track {
|
||||||
|
/* Background */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
&-item {
|
&-item {
|
||||||
@ -152,7 +157,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.query-example {
|
.query-example {
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
font-size: .95rem;
|
font-size: 0.95rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-form v-if="app.isSignedIn" class="profile-settings" @submit.stop="updateProfile">
|
<q-form
|
||||||
<h3>Profile</h3>
|
v-if="app.isSignedIn"
|
||||||
|
class="profile-settings"
|
||||||
|
@submit.stop="updateProfile"
|
||||||
|
>
|
||||||
|
<h3>{{ $t("Profile") }}</h3>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<q-input v-model="name" label="Name" maxlength="64" dense />
|
<q-input v-model="name" :label="$t('Name')" maxlength="64" dense />
|
||||||
</div>
|
</div>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<q-input v-model="about" label="About" maxlength="150" autogrow dense />
|
<q-input
|
||||||
|
v-model="about"
|
||||||
|
:label="$t('About')"
|
||||||
|
maxlength="150"
|
||||||
|
autogrow
|
||||||
|
dense
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<q-input v-model="picture" label="Picture URL" dense />
|
<q-input v-model="picture" :label="$t('Picture URL')" dense />
|
||||||
<img v-if="picture" :src="picture" class="picture-preview" loading="lazy" />
|
<img
|
||||||
|
v-if="picture"
|
||||||
|
:src="picture"
|
||||||
|
class="picture-preview"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<q-input v-model="nip05" label="NIP05 Identifier" dense />
|
<q-input v-model="nip05" :label="$t('NIP05 Identifier')" dense />
|
||||||
<q-icon v-if="verified" name="verified" class="nip05-verified" size="sm" />
|
<q-icon
|
||||||
|
v-if="verified"
|
||||||
|
name="verified"
|
||||||
|
class="nip05-verified"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" :disabled="!changed" class="btn btn-sm btn-primary">Save</button>
|
<button type="submit" :disabled="!changed" class="btn btn-sm btn-primary">
|
||||||
<button class="btn btn-sm" :disabled="!changed" @click="setDataFromProfile">Reset</button>
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm"
|
||||||
|
:disabled="!changed"
|
||||||
|
@click="setDataFromProfile"
|
||||||
|
>
|
||||||
|
{{ $t("Reset") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import Nip05 from 'src/utils/Nip05'
|
import Nip05 from 'src/utils/Nip05'
|
||||||
import EventBuilder from 'src/nostr/EventBuilder'
|
import EventBuilder from 'src/nostr/EventBuilder'
|
||||||
|
import { $t } from 'src/boot/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProfileSettings',
|
name: 'ProfileSettings',
|
||||||
@ -55,18 +84,20 @@ export default {
|
|||||||
return this.nostr.getProfile(this.pubkey)
|
return this.nostr.getProfile(this.pubkey)
|
||||||
},
|
},
|
||||||
changed() {
|
changed() {
|
||||||
return this.name !== (this.profile?.name || '')
|
return (
|
||||||
|| this.about !== (this.profile?.about || '')
|
this.name !== (this.profile?.name || '') ||
|
||||||
|| this.picture !== (this.profile?.picture || '')
|
this.about !== (this.profile?.about || '') ||
|
||||||
|| this.nip05 !== (this.profile?.nip05?.url || '')
|
this.picture !== (this.profile?.picture || '') ||
|
||||||
|
this.nip05 !== (this.profile?.nip05?.url || '')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setDataFromProfile() {
|
setDataFromProfile() {
|
||||||
this.name = (this.profile?.name || '')
|
this.name = this.profile?.name || ''
|
||||||
this.about = (this.profile?.about || '')
|
this.about = this.profile?.about || ''
|
||||||
this.picture = (this.profile?.picture || '')
|
this.picture = this.profile?.picture || ''
|
||||||
this.nip05 = (this.profile?.nip05.url || '')
|
this.nip05 = this.profile?.nip05.url || ''
|
||||||
this.verified = this.profile?.nip05.verified
|
this.verified = this.profile?.nip05.verified
|
||||||
},
|
},
|
||||||
async updateProfile() {
|
async updateProfile() {
|
||||||
@ -77,11 +108,11 @@ export default {
|
|||||||
nip05: this.nip05 || undefined,
|
nip05: this.nip05 || undefined,
|
||||||
}
|
}
|
||||||
const event = EventBuilder.metadata(this.pubkey, metadata).build()
|
const event = EventBuilder.metadata(this.pubkey, metadata).build()
|
||||||
if (!await this.app.signEvent(event)) return
|
if (!(await this.app.signEvent(event))) return
|
||||||
if (!await this.nostr.publish(event)) {
|
if (!(await this.nostr.publish(event))) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Failed to update profile',
|
message: $t('Failed to update profile'),
|
||||||
color: 'negative'
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -92,11 +123,11 @@ export default {
|
|||||||
},
|
},
|
||||||
async nip05() {
|
async nip05() {
|
||||||
this.verified = await Nip05.verify(this.pubkey, this.nip05)
|
this.verified = await Nip05.verify(this.pubkey, this.nip05)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setDataFromProfile()
|
this.setDataFromProfile()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -134,7 +165,7 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
button + button {
|
button + button {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,11 +176,12 @@ export default {
|
|||||||
.profile-settings .input {
|
.profile-settings .input {
|
||||||
.q-field__label {
|
.q-field__label {
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
margin: 0 .5rem;
|
margin: 0 0.5rem;
|
||||||
}
|
}
|
||||||
input, textarea {
|
input,
|
||||||
|
textarea {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0 .5rem;
|
padding: 0 0.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.q-field__control {
|
.q-field__control {
|
||||||
|
@ -1,14 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relay-settings">
|
<div class="relay-settings">
|
||||||
<h3>Relays</h3>
|
<h3>{{ $t("Relays") }}</h3>
|
||||||
<div v-for="relay in settings.relays" :key="relay" class="relay">
|
<div v-for="relay in settings.relays" :key="relay" class="relay">
|
||||||
<span class="relay-url">{{ relay }}</span>
|
<span class="relay-url">{{ relay }}</span>
|
||||||
<!-- <q-icon v-if="isConnected(relay)" icon="fiber_manual_record" size="sm" class="connected" />-->
|
<!-- <q-icon v-if="isConnected(relay)" icon="fiber_manual_record" size="sm" class="connected" />-->
|
||||||
<q-btn icon="delete_outline" size="sm" class="btn-icon" flat round @click="removeRelay(relay)" />
|
|
||||||
|
<q-btn
|
||||||
|
icon="delete_outline"
|
||||||
|
size="sm"
|
||||||
|
class="btn-icon"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
@click="removeRelay(relay)"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ $t("Delete relay") }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<q-form class="add-relay" @submit.stop="addRelay">
|
<q-form class="add-relay" @submit.stop="addRelay">
|
||||||
<q-input v-model="newRelayUrl" label="Add a relay" dense />
|
<q-input v-model="newRelayUrl" :label="$t('Add relay')" dense />
|
||||||
<q-btn type="submit" icon="add_circle_outline" size="sm" flat round class="btn-icon" />
|
<q-btn
|
||||||
|
type="submit"
|
||||||
|
icon="add_circle_outline"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
:disabled="!newRelayUrl"
|
||||||
|
class="btn-icon"
|
||||||
|
/>
|
||||||
</q-form>
|
</q-form>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button
|
<button
|
||||||
@ -16,15 +34,15 @@
|
|||||||
:disabled="!changed"
|
:disabled="!changed"
|
||||||
@click="settings.restoreDefaultRelays()"
|
@click="settings.restoreDefaultRelays()"
|
||||||
>
|
>
|
||||||
Restore defaults
|
{{ $t("Restore defaults") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
import {Notify} from 'quasar'
|
import { Notify } from 'quasar'
|
||||||
// import {useNostrStore} from 'src/nostr/NostrStore'
|
// import {useNostrStore} from 'src/nostr/NostrStore'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -43,7 +61,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
changed() {
|
changed() {
|
||||||
return !this.settings.hasDefaultRelays()
|
return !this.settings.hasDefaultRelays()
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addRelay() {
|
addRelay() {
|
||||||
@ -53,14 +71,14 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
message: 'Invalid URL',
|
message: 'Invalid URL',
|
||||||
color: 'negative'
|
color: 'negative',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (url.protocol !== 'wss:') {
|
if (url.protocol !== 'wss:') {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
message: 'Must be a wss:// URL',
|
message: 'Must be a wss:// URL',
|
||||||
color: 'negative'
|
color: 'negative',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -71,7 +89,7 @@ export default {
|
|||||||
if (this.settings.hasRelay(href)) {
|
if (this.settings.hasRelay(href)) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
message: 'Relay already exists',
|
message: 'Relay already exists',
|
||||||
color: 'negative'
|
color: 'negative',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -84,7 +102,7 @@ export default {
|
|||||||
// isConnected(url) {
|
// isConnected(url) {
|
||||||
// return this.nostr.client.isConnectedTo(url)
|
// return this.nostr.client.isConnectedTo(url)
|
||||||
// }
|
// }
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -100,7 +118,7 @@ export default {
|
|||||||
.relay {
|
.relay {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: .5rem;
|
padding: 0.5rem;
|
||||||
border-bottom: $border-dark;
|
border-bottom: $border-dark;
|
||||||
transition: 200ms ease;
|
transition: 200ms ease;
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -121,7 +139,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .5rem;
|
right: 0.5rem;
|
||||||
top: 7px;
|
top: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +154,7 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
button + button {
|
button + button {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,11 +165,11 @@ export default {
|
|||||||
.relay-settings .add-relay {
|
.relay-settings .add-relay {
|
||||||
.q-field__label {
|
.q-field__label {
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
margin: 0 .5rem;
|
margin: 0 0.5rem;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0 .5rem;
|
padding: 0 0.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.q-field__control {
|
.q-field__control {
|
||||||
|
@ -2,11 +2,9 @@
|
|||||||
<div v-if="app.isSignedIn && contacts?.length" class="following">
|
<div v-if="app.isSignedIn && contacts?.length" class="following">
|
||||||
<div class="following-wrapper">
|
<div class="following-wrapper">
|
||||||
<div class="following-header">
|
<div class="following-header">
|
||||||
<h3>Following</h3>
|
<h3>{{ $t("Following") }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="following-body">
|
||||||
class="following-body"
|
|
||||||
>
|
|
||||||
<UserCard
|
<UserCard
|
||||||
v-for="contact in contacts"
|
v-for="contact in contacts"
|
||||||
:key="contact.pubkey"
|
:key="contact.pubkey"
|
||||||
@ -23,13 +21,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import UserCard from 'components/User/UserCard.vue'
|
import UserCard from 'components/User/UserCard.vue'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import routerMixin from 'src/router/mixin'
|
import routerMixin from 'src/router/mixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FollowingBox',
|
name: 'FollowingBox',
|
||||||
components: {UserCard},
|
components: { UserCard },
|
||||||
mixins: [routerMixin],
|
mixins: [routerMixin],
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -43,7 +41,7 @@ export default {
|
|||||||
},
|
},
|
||||||
contacts() {
|
contacts() {
|
||||||
return this.nostr.getContacts(this.pubkey)?.slice(0, 20)
|
return this.nostr.getContacts(this.pubkey)?.slice(0, 20)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -74,10 +72,12 @@ export default {
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb { /* Foreground */
|
&::-webkit-scrollbar-thumb {
|
||||||
|
/* Foreground */
|
||||||
background: $color-dark-gray;
|
background: $color-dark-gray;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-track { /* Background */
|
&::-webkit-scrollbar-track {
|
||||||
|
/* Background */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
&-item {
|
&-item {
|
||||||
@ -104,7 +104,11 @@ export default {
|
|||||||
right: 0;
|
right: 0;
|
||||||
height: 1.2rem;
|
height: 1.2rem;
|
||||||
border-radius: 0 0 1rem 1rem;
|
border-radius: 0 0 1rem 1rem;
|
||||||
background: linear-gradient(180deg, rgba(29, 41, 53, 0), rgba(29, 41, 53, 1));
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(29, 41, 53, 0),
|
||||||
|
rgba(29, 41, 53, 1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,23 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome" v-if="!app.isSignedIn">
|
<div class="welcome" v-if="!app.isSignedIn">
|
||||||
<div class="welcome-header">
|
<div class="welcome-header">
|
||||||
<h3>New to Nostr?</h3>
|
<h3>{{ $t("New to Nostr?") }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="welcome-content">
|
<div class="welcome-content">
|
||||||
<button v-if="nip07available" class="btn btn-primary" @click.stop="signInNip07()">Log in with Extension</button>
|
<button
|
||||||
<button class="btn" :class="{'btn-primary': !nip07available}" @click.stop="signUp">
|
v-if="nip07available"
|
||||||
Create Account
|
class="btn btn-primary"
|
||||||
|
@click.stop="signInNip07()"
|
||||||
|
>
|
||||||
|
{{ $t("Log in with Extension") }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="!nip07available" class="btn" @click.stop="signIn">Log in</button>
|
<button
|
||||||
<a v-else @click.stop="signIn">Log in with key</a>
|
class="btn"
|
||||||
|
:class="{ 'btn-primary': !nip07available }"
|
||||||
|
@click.stop="signUp"
|
||||||
|
>
|
||||||
|
{{ $t("Create Account") }}
|
||||||
|
</button>
|
||||||
|
<button v-if="!nip07available" class="btn" @click.stop="signIn">
|
||||||
|
{{ $t("Log in") }}
|
||||||
|
</button>
|
||||||
|
<a v-else @click.stop="signIn"> {{ $t("Log in with key") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
import Nip07 from 'src/utils/Nip07'
|
import Nip07 from 'src/utils/Nip07'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -56,8 +68,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.nip07available = Nip07.isAvailable()
|
this.nip07available = Nip07.isAvailable()
|
||||||
setTimeout(() => this.nip07available = Nip07.isAvailable(), 300)
|
setTimeout(() => (this.nip07available = Nip07.isAvailable()), 300)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -82,7 +94,7 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
button {
|
button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: .5rem;
|
padding: 0.5rem;
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@ -98,5 +110,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="app.signInDialog.open" @before-show="updateFragment" @hide="onClose" ref="signInDialog">
|
<q-dialog
|
||||||
|
v-model="app.signInDialog.open"
|
||||||
|
@before-show="updateFragment"
|
||||||
|
@hide="onClose"
|
||||||
|
ref="signInDialog"
|
||||||
|
>
|
||||||
<div class="sign-in-dialog">
|
<div class="sign-in-dialog">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="showClose"
|
v-if="showClose"
|
||||||
@ -29,17 +34,39 @@
|
|||||||
<p class="prompt">
|
<p class="prompt">
|
||||||
{{ prompt }}
|
{{ prompt }}
|
||||||
</p>
|
</p>
|
||||||
<button v-if="nip07available" class="btn btn-primary" @click.stop="signInNip07()">Log in with Extension</button>
|
<button
|
||||||
<button class="btn" :class="{'btn-primary': !nip07available}" @click.stop="fragment = 'sign-up'">
|
v-if="nip07available"
|
||||||
Create Account
|
class="btn btn-primary"
|
||||||
|
@click.stop="signInNip07()"
|
||||||
|
>
|
||||||
|
{{ $t("Log in with Extension") }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="!nip07available" class="btn" @click.stop="fragment = 'sign-in'">Log in</button>
|
<button
|
||||||
<a v-else @click.stop="fragment = 'sign-in'">Log in with key</a>
|
class="btn"
|
||||||
|
:class="{ 'btn-primary': !nip07available }"
|
||||||
|
@click.stop="fragment = 'sign-up'"
|
||||||
|
>
|
||||||
|
{{ $t("Create Account") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!nip07available"
|
||||||
|
class="btn"
|
||||||
|
@click.stop="fragment = 'sign-in'"
|
||||||
|
>
|
||||||
|
{{ $t("Log in") }}
|
||||||
|
</button>
|
||||||
|
<a v-else @click.stop="fragment = 'sign-in'">{{
|
||||||
|
$t("Log in with key")
|
||||||
|
}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SignUpForm v-if="fragment === 'sign-up'" @complete="onComplete" />
|
<SignUpForm v-if="fragment === 'sign-up'" @complete="onComplete" />
|
||||||
<SignInForm v-if="fragment === 'sign-in'" @complete="onComplete"/>
|
<SignInForm v-if="fragment === 'sign-in'" @complete="onComplete" />
|
||||||
<SignInForm v-if="fragment === 'private-key'" @complete="onComplete" private-key-only />
|
<SignInForm
|
||||||
|
v-if="fragment === 'private-key'"
|
||||||
|
@complete="onComplete"
|
||||||
|
private-key-only
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
@ -49,8 +76,8 @@ import Logo from 'components/Logo.vue'
|
|||||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||||
import SignUpForm from 'components/SignIn/SignUpForm.vue'
|
import SignUpForm from 'components/SignIn/SignUpForm.vue'
|
||||||
import SignInForm from 'components/SignIn/SignInForm.vue'
|
import SignInForm from 'components/SignIn/SignInForm.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
import Nip07 from 'src/utils/Nip07'
|
import Nip07 from 'src/utils/Nip07'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -59,13 +86,13 @@ export default {
|
|||||||
Logo,
|
Logo,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
SignInForm,
|
SignInForm,
|
||||||
SignUpForm
|
SignUpForm,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
prompt: {
|
prompt: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -89,7 +116,7 @@ export default {
|
|||||||
},
|
},
|
||||||
nip07available() {
|
nip07available() {
|
||||||
return Nip07.isAvailable()
|
return Nip07.isAvailable()
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClose() {
|
onClose() {
|
||||||
@ -99,7 +126,7 @@ export default {
|
|||||||
this.fragment = 'welcome'
|
this.fragment = 'welcome'
|
||||||
this.pubkey = null
|
this.pubkey = null
|
||||||
},
|
},
|
||||||
onComplete({pubkey}) {
|
onComplete({ pubkey }) {
|
||||||
this.pubkey = pubkey
|
this.pubkey = pubkey
|
||||||
this.$refs.signInDialog.hide()
|
this.$refs.signInDialog.hide()
|
||||||
},
|
},
|
||||||
@ -117,7 +144,7 @@ export default {
|
|||||||
this.settings.addAccount(account)
|
this.settings.addAccount(account)
|
||||||
this.app.switchAccount(pubkey)
|
this.app.switchAccount(pubkey)
|
||||||
|
|
||||||
this.onComplete({pubkey})
|
this.onComplete({ pubkey })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -137,8 +164,8 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
top: .5rem;
|
top: 0.5rem;
|
||||||
left: .5rem;
|
left: 0.5rem;
|
||||||
fill: #fff;
|
fill: #fff;
|
||||||
}
|
}
|
||||||
.logo {
|
.logo {
|
||||||
@ -178,5 +205,4 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,28 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sign-in">
|
<div class="sign-in">
|
||||||
<h3>{{ header }}</h3>
|
<h3>{{ $t(header) }}</h3>
|
||||||
<q-form @submit.stop="signIn">
|
<q-form @submit.stop="signIn">
|
||||||
<label for="private-key">{{ prompt }}</label>
|
<label for="private-key">{{ $t(prompt) }}</label>
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="key"
|
v-model="key"
|
||||||
:placeholder="placeholder"
|
:placeholder="$t(placeholder)"
|
||||||
maxlength="63"
|
maxlength="63"
|
||||||
:class="{
|
:class="{
|
||||||
valid: validKey,
|
valid: validKey,
|
||||||
invalid: invalidKey,
|
invalid: invalidKey,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="!validKey">{{ buttonLabel }}</button>
|
<button type="submit" class="btn btn-primary" :disabled="!validKey">
|
||||||
|
{{ $t(buttonLabel) }}
|
||||||
|
</button>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {decode as bech32decode} from 'bech32-buffer'
|
import { decode as bech32decode } from 'bech32-buffer'
|
||||||
import {bech32prefix, bech32ToHex} from 'src/utils/utils'
|
import { bech32prefix, bech32ToHex } from 'src/utils/utils'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignInForm',
|
name: 'SignInForm',
|
||||||
@ -31,7 +33,7 @@ export default {
|
|||||||
privateKeyOnly: {
|
privateKeyOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -40,44 +42,35 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
header() {
|
header() {
|
||||||
// TODO i18n
|
return this.privateKeyOnly ? 'Private key needed' : 'Log in'
|
||||||
return this.privateKeyOnly
|
|
||||||
? 'Private key needed'
|
|
||||||
: 'Log in'
|
|
||||||
},
|
},
|
||||||
prompt() {
|
prompt() {
|
||||||
// TODO i18n
|
|
||||||
return this.privateKeyOnly
|
return this.privateKeyOnly
|
||||||
? 'Paste your private key to continue'
|
? 'Paste your private key to continue'
|
||||||
: 'Paste your public or private key'
|
: 'Paste your public or private key'
|
||||||
},
|
},
|
||||||
placeholder() {
|
placeholder() {
|
||||||
// TODO i18n
|
return this.privateKeyOnly ? 'nsec…' : 'npub… / nsec…'
|
||||||
return this.privateKeyOnly
|
|
||||||
? 'nsec…'
|
|
||||||
: 'npub… / nsec…'
|
|
||||||
},
|
},
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
// TODO i18n
|
return this.privateKeyOnly ? 'Continue' : 'Log in'
|
||||||
return this.privateKeyOnly
|
|
||||||
? 'Continue'
|
|
||||||
: 'Log in'
|
|
||||||
},
|
},
|
||||||
validKey() {
|
validKey() {
|
||||||
return this.isValidKey(this.key)
|
return this.isValidKey(this.key)
|
||||||
},
|
},
|
||||||
invalidKey() {
|
invalidKey() {
|
||||||
return this.key
|
return this.key && this.key.length >= 63 && !this.isValidKey(this.key)
|
||||||
&& this.key.length >= 63
|
},
|
||||||
&& !this.isValidKey(this.key)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isValidKey(str) {
|
isValidKey(str) {
|
||||||
if (!str) return false
|
if (!str) return false
|
||||||
try {
|
try {
|
||||||
const {data, prefix} = bech32decode(str.toLowerCase())
|
const { data, prefix } = bech32decode(str.toLowerCase())
|
||||||
return data.byteLength === 32 && ((prefix === 'npub' && !this.privateKeyOnly) || prefix === 'nsec')
|
return (
|
||||||
|
data.byteLength === 32 &&
|
||||||
|
((prefix === 'npub' && !this.privateKeyOnly) || prefix === 'nsec')
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -87,15 +80,15 @@ export default {
|
|||||||
|
|
||||||
let opts
|
let opts
|
||||||
if (bech32prefix(this.key) === 'npub') {
|
if (bech32prefix(this.key) === 'npub') {
|
||||||
opts = {pubkey: bech32ToHex(this.key)}
|
opts = { pubkey: bech32ToHex(this.key) }
|
||||||
} else {
|
} else {
|
||||||
opts = {privkey: bech32ToHex(this.key)}
|
opts = { privkey: bech32ToHex(this.key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = useSettingsStore().addAccount(opts)
|
const account = useSettingsStore().addAccount(opts)
|
||||||
useAppStore().switchAccount(account.pubkey)
|
useAppStore().switchAccount(account.pubkey)
|
||||||
|
|
||||||
this.$emit('complete', {pubkey: account.pubkey})
|
this.$emit('complete', { pubkey: account.pubkey })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -1,20 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sign-up">
|
<div class="sign-up">
|
||||||
<h3>Create Account</h3>
|
<h3>{{ $t("Create Account") }}</h3>
|
||||||
<q-form @submit.stop="signUp">
|
<q-form @submit.stop="signUp">
|
||||||
<label for="username">What's your name?</label>
|
<label for="username">{{ $t("What's your name?") }}</label>
|
||||||
<input v-model="username" ref="input" id="username" autocomplete="false" />
|
<input
|
||||||
<button type="submit" class="btn btn-primary" :disabled="!validUsername">Create</button>
|
v-model="username"
|
||||||
|
ref="input"
|
||||||
|
id="username"
|
||||||
|
autocomplete="false"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="!validUsername">
|
||||||
|
{{ $t("Create") }}
|
||||||
|
</button>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
import {generatePrivateKey} from 'nostr-tools'
|
import { generatePrivateKey } from 'nostr-tools'
|
||||||
import EventBuilder from 'src/nostr/EventBuilder'
|
import EventBuilder from 'src/nostr/EventBuilder'
|
||||||
|
import { $t } from 'src/boot/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignUpForm',
|
name: 'SignUpForm',
|
||||||
@ -36,28 +44,30 @@ export default {
|
|||||||
const privkey = generatePrivateKey()
|
const privkey = generatePrivateKey()
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const account = settings.addAccount({privkey})
|
const account = settings.addAccount({ privkey })
|
||||||
const app = useAppStore()
|
const app = useAppStore()
|
||||||
app.switchAccount(account.pubkey)
|
app.switchAccount(account.pubkey)
|
||||||
|
|
||||||
const event = EventBuilder.metadata(account.pubkey, {name: this.username}).build()
|
const event = EventBuilder.metadata(account.pubkey, {
|
||||||
|
name: this.username,
|
||||||
|
}).build()
|
||||||
await app.signEvent(event)
|
await app.signEvent(event)
|
||||||
if (await useNostrStore().publish(event)) {
|
if (await useNostrStore().publish(event)) {
|
||||||
this.$emit('complete', {
|
this.$emit('complete', {
|
||||||
pubkey: account.pubkey,
|
pubkey: account.pubkey,
|
||||||
name: this.username
|
name: this.username,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Failed to create profile',
|
message: $t('Failed to create profile'),
|
||||||
color: 'negative',
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,17 +2,18 @@
|
|||||||
<button
|
<button
|
||||||
v-if="app.isSignedIn"
|
v-if="app.isSignedIn"
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
:class="{'btn-primary': !isFollowing}"
|
:class="{ 'btn-primary': !isFollowing }"
|
||||||
@click="toggleFollow"
|
@click="toggleFollow"
|
||||||
>
|
>
|
||||||
{{ isFollowing ? 'Unfollow' : 'Follow' }}
|
{{ $t(isFollowing ? "Unfollow" : "Follow") }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import EventBuilder from 'src/nostr/EventBuilder'
|
import EventBuilder from 'src/nostr/EventBuilder'
|
||||||
|
import { $t } from 'src/boot/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FollowButton',
|
name: 'FollowButton',
|
||||||
@ -20,7 +21,7 @@ export default {
|
|||||||
pubkey: {
|
pubkey: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -34,33 +35,39 @@ export default {
|
|||||||
},
|
},
|
||||||
isFollowing() {
|
isFollowing() {
|
||||||
// TODO improve
|
// TODO improve
|
||||||
return this.contacts?.length && this.contacts.find(contact => contact.pubkey === this.pubkey)
|
return (
|
||||||
|
this.contacts?.length &&
|
||||||
|
this.contacts.find((contact) => contact.pubkey === this.pubkey)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async updateContacts(contacts) {
|
async updateContacts(contacts) {
|
||||||
const event = EventBuilder.contacts(this.app.myPubkey, contacts.map(c => c.pubkey)).build()
|
const event = EventBuilder.contacts(
|
||||||
if (!await this.app.signEvent(event)) return
|
this.app.myPubkey,
|
||||||
if (!await this.nostr.publish(event)) {
|
contacts.map((c) => c.pubkey)
|
||||||
|
).build()
|
||||||
|
if (!(await this.app.signEvent(event))) return
|
||||||
|
if (!(await this.nostr.publish(event))) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: 'Failed to update followers',
|
message: $t('Failed to update followers'),
|
||||||
color: 'negative',
|
color: 'negative',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleFollow() {
|
toggleFollow() {
|
||||||
return this.isFollowing
|
return this.isFollowing ? this.unfollow() : this.follow()
|
||||||
? this.unfollow()
|
|
||||||
: this.follow()
|
|
||||||
},
|
},
|
||||||
async follow() {
|
async follow() {
|
||||||
const contacts = [].concat(this.contacts || []) // Clone array
|
const contacts = [].concat(this.contacts || []) // Clone array
|
||||||
contacts.push({pubkey: this.pubkey})
|
contacts.push({ pubkey: this.pubkey })
|
||||||
await this.updateContacts(contacts)
|
await this.updateContacts(contacts)
|
||||||
},
|
},
|
||||||
async unfollow() {
|
async unfollow() {
|
||||||
const contacts = [].concat(this.contacts || []) // Clone array
|
const contacts = [].concat(this.contacts || []) // Clone array
|
||||||
const idx = contacts.findIndex(contact => contact.pubkey === this.pubkey)
|
const idx = contacts.findIndex(
|
||||||
|
(contact) => contact.pubkey === this.pubkey
|
||||||
|
)
|
||||||
contacts.splice(idx, 1)
|
contacts.splice(idx, 1)
|
||||||
await this.updateContacts(contacts)
|
await this.updateContacts(contacts)
|
||||||
},
|
},
|
||||||
@ -69,5 +76,4 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="dialogOpen">
|
<q-dialog v-model="dialogOpen">
|
||||||
<div class="logout-dialog">
|
<div class="logout-dialog">
|
||||||
<q-btn icon="close" size="md" class="icon" flat round v-close-popup/>
|
<q-btn icon="close" size="md" class="icon" flat round v-close-popup />
|
||||||
|
<h3>
|
||||||
<h3>Log out from <UserName :pubkey="pubkey" /></h3>
|
{{ $t("Do you really want to log out from") }}
|
||||||
<p>
|
<UserName :pubkey="pubkey" />?
|
||||||
Do you really want to log out from <UserName :pubkey="pubkey" />?
|
</h3>
|
||||||
</p>
|
|
||||||
<p v-if="privateKey" class="warning">
|
<p v-if="privateKey" class="warning">
|
||||||
<span class="warning-icon"><q-icon name="warning" size="lg" /></span>
|
<span class="warning-icon"><q-icon name="warning" size="lg" /></span>
|
||||||
<span class="warning-content">
|
<span class="warning-content">
|
||||||
Make sure you have a backup of your private key! Otherwise it is impossible to log back in to your account.
|
{{
|
||||||
|
$t(
|
||||||
|
"Make sure you have a backup of your private key! Otherwise it is impossible to log back in to your account."
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<input v-if="privateKey" :value="hexToBech32(privateKey, 'nsec')" readonly />
|
<input
|
||||||
|
v-if="privateKey"
|
||||||
|
:value="hexToBech32(privateKey, 'nsec')"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="btn btn-sm btn-primary" @click="logout" v-close-popup>Log out</button>
|
<button class="btn btn-sm btn-primary" @click="logout" v-close-popup>
|
||||||
<button class="btn btn-sm" v-close-popup>Cancel</button>
|
{{ $t("Log out") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm" v-close-popup>{{ $t("Cancel") }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
@ -24,14 +33,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import { useSettingsStore } from 'stores/Settings'
|
||||||
import {hexToBech32} from 'src/utils/utils'
|
import { hexToBech32 } from 'src/utils/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LogoutDialog',
|
name: 'LogoutDialog',
|
||||||
components: {
|
components: {
|
||||||
UserName
|
UserName,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
pubkey: {
|
pubkey: {
|
||||||
@ -51,7 +60,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
privateKey() {
|
privateKey() {
|
||||||
return useAppStore().activeAccount.privkey
|
return useAppStore().activeAccount.privkey
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hexToBech32,
|
hexToBech32,
|
||||||
@ -63,7 +72,7 @@ export default {
|
|||||||
},
|
},
|
||||||
dismiss() {
|
dismiss() {
|
||||||
this.dialogOpen = false
|
this.dialogOpen = false
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -83,17 +92,17 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
top: .5rem;
|
top: 0.5rem;
|
||||||
left: .5rem;
|
left: 0.5rem;
|
||||||
fill: #fff;
|
fill: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
padding: 0 .5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
> p {
|
> p {
|
||||||
padding: 0 .5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="verified" class="nip05-badge">
|
<span v-if="verified" class="nip05-badge">
|
||||||
<q-icon name="verified" :size="size" color="primary">
|
<q-icon name="verified" :size="size" color="primary">
|
||||||
<q-tooltip>NIP05 verified</q-tooltip>
|
<q-tooltip>{{ $t("NIP05 verified") }}</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
<span class="nip05-badge-text">{{ nip05 }}</span>
|
<span class="nip05-badge-text">{{ nip05 }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Nip05Badge',
|
name: 'Nip05Badge',
|
||||||
@ -19,8 +19,8 @@ export default {
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '14px'
|
default: '14px',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
@ -40,18 +40,22 @@ export default {
|
|||||||
if (!this.profile?.nip05.url) return
|
if (!this.profile?.nip05.url) return
|
||||||
return this.profile.nip05.url
|
return this.profile.nip05.url
|
||||||
.split('@')
|
.split('@')
|
||||||
.filter(part => part !== '_' && part?.toLowerCase() !== this.profile.name?.toLowerCase())
|
.filter(
|
||||||
|
(part) =>
|
||||||
|
part !== '_' &&
|
||||||
|
part?.toLowerCase() !== this.profile.name?.toLowerCase()
|
||||||
|
)
|
||||||
.join('@')
|
.join('@')
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async profile() {
|
async profile() {
|
||||||
this.verified = await this.profile?.isNip05Verified()
|
this.verified = await this.profile?.isNip05Verified()
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.verified = await this.profile?.isNip05Verified()
|
this.verified = await this.profile?.isNip05Verified()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
// so you can safely delete all default props below
|
// so you can safely delete all default props below
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'thread': 'Thread'
|
thread: 'Thread',
|
||||||
|
'Load {unread} unread': 'Load {unread} unread',
|
||||||
}
|
}
|
93
src/i18n/es/index.js
Normal file
93
src/i18n/es/index.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// This is just an example,
|
||||||
|
// so you can safely delete all default props below
|
||||||
|
|
||||||
|
export default {
|
||||||
|
About: 'Presentación',
|
||||||
|
'Add an account': 'Agregar cuenta',
|
||||||
|
'Add relay': 'Agregar relay',
|
||||||
|
April: 'Abril',
|
||||||
|
August: 'Agosto',
|
||||||
|
Cancels: 'Cancelar',
|
||||||
|
'Click to decrypt': 'Click para desencriptar',
|
||||||
|
Close: 'Cerrar',
|
||||||
|
Continue: 'Continuar',
|
||||||
|
Create: 'Crear',
|
||||||
|
'Create Account': 'Crear una cuenta',
|
||||||
|
December: 'Diciembre',
|
||||||
|
'Decryption failed': 'Fallo al desencriptar',
|
||||||
|
'Delete relay': 'Eliminar relay',
|
||||||
|
'Do you really want to log out from': '¿Quieres Cerrar sesión de',
|
||||||
|
'Failed to create profile': 'Fallo al crear el perfil',
|
||||||
|
'Failed to delete reaction': 'Fallo al eliminar reacción',
|
||||||
|
'Failed to publish post': 'No se pudo publicar la publicación',
|
||||||
|
'Failed to publish reaction': 'Fallo al publicar reacción',
|
||||||
|
'Failed to send message': 'Fallo al enviar mensaje',
|
||||||
|
'Failed to update followers': 'Fallo al actualizar lista de seguidores',
|
||||||
|
'Failed to update profile': 'Fallo al actualizar el perfil',
|
||||||
|
February: 'Febrero',
|
||||||
|
Follow: 'Seguir',
|
||||||
|
Followers: 'Seguidores',
|
||||||
|
Following: 'Seguidos',
|
||||||
|
Global: 'Global',
|
||||||
|
Hilo: 'Hilo',
|
||||||
|
Home: 'Inicio',
|
||||||
|
"icon in the recipient's profile.": 'en elperfil del destinatario.',
|
||||||
|
January: 'Enero',
|
||||||
|
'Jot something down': 'Apunta algo',
|
||||||
|
July: 'Julio',
|
||||||
|
June: 'Junio',
|
||||||
|
'Load {unread} unread': 'Cargar {unread} sin leer',
|
||||||
|
'Load more': 'Cargar más..',
|
||||||
|
'Loading...': 'Cargando...',
|
||||||
|
'Log in': 'Ingresar',
|
||||||
|
'Log in with Extension': 'Ingresar con extensión',
|
||||||
|
'Log in with key': 'Ingresar con clave',
|
||||||
|
'Log out': 'Cerrar sesión',
|
||||||
|
'Logout from': 'Cerrar sesión en',
|
||||||
|
'Make sure you have a backup of your private key! Otherwise it is impossible to log back in to your account.': '¡Asegúrate de tener una copia de seguridad de tu clave privada! De lo contrario, es imposible volver a iniciar sesión en su cuenta.',
|
||||||
|
March: 'Marzo',
|
||||||
|
'Mark all as read': 'Marcar todo como leido',
|
||||||
|
May: 'Mayo',
|
||||||
|
Message: 'Mensaje',
|
||||||
|
Messages: 'Mensajes',
|
||||||
|
Name: 'Nombre',
|
||||||
|
'New to Nostr?': '¿Nuevo en Nostr?',
|
||||||
|
'NIP05 Identifier': 'Identificador NIP05',
|
||||||
|
'NIP05 verified': 'NIP05 verificado',
|
||||||
|
'No more items. Try again?': 'No hay registros. ¿Intentar otra vez?',
|
||||||
|
'Nothing found.': 'No hay registros.',
|
||||||
|
'Nothing here.': 'Nada por aquí',
|
||||||
|
Notifications: 'Notificationes',
|
||||||
|
November: 'Noviembre',
|
||||||
|
October: 'Octubre',
|
||||||
|
'Paste your private key to continue': 'Pega tu clave privada para continuar',
|
||||||
|
'Paste your public or private key': 'Pega tu clave pública o privada',
|
||||||
|
'Picture URL': 'Imagen de perfil',
|
||||||
|
Post: 'Postear',
|
||||||
|
'Post published to {numRelays} relays': 'Posteo publicado en {numRelays} relays',
|
||||||
|
'Post your reply': 'Postea tu respuesta',
|
||||||
|
Posts: 'Posteos',
|
||||||
|
'Private key needed': 'Clave privada requira',
|
||||||
|
Profile: 'Perfil',
|
||||||
|
Reactions: 'Reacciones',
|
||||||
|
Relays: 'Relays',
|
||||||
|
Replies: 'Respuestas',
|
||||||
|
'Reply published to {numRelays} relays': 'Repuesta publicada en {numRelays} relays',
|
||||||
|
'Replying to': 'En respuesta a',
|
||||||
|
Reset: 'Resetear',
|
||||||
|
'Restore defaults': 'Restaurar predeterminados',
|
||||||
|
Save: 'Guardar',
|
||||||
|
'Search profiles': 'Buscar perfiles',
|
||||||
|
'Send private message': 'Enviar mensaje privado',
|
||||||
|
September: 'Septiembre',
|
||||||
|
Settings: 'Preferencias',
|
||||||
|
'This is the beginning of your message history with': 'Este es el comienzo de su historial de mensajes con',
|
||||||
|
thread: 'Hilo',
|
||||||
|
Thread: 'Hilo',
|
||||||
|
'Tip with Bitcoin Lightning': 'Dar propina con Bitcoin Lightning',
|
||||||
|
'To send a message, click on the': 'Para enviar un mensaje, haga clic en el icono',
|
||||||
|
'Try again?': '¿Intentar otra vez?',
|
||||||
|
Unfollow: 'Dejar de seguir',
|
||||||
|
"What's happening?": '¿Qué estas pensando?',
|
||||||
|
"What's your name?": 'Cual es tu nombre?'
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import enUS from './en-US'
|
import en from './en'
|
||||||
|
import es from './es'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': enUS
|
en,
|
||||||
|
es,
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
class="feed-selector"
|
class="feed-selector"
|
||||||
:options="[
|
:options="[
|
||||||
{value: 'following', icon: 'group'},
|
{ value: 'following', icon: 'group', title: $t('Following') },
|
||||||
{value: 'global', icon: 'public'},
|
{ value: 'global', icon: 'public', title: $t('Global') },
|
||||||
]"
|
]"
|
||||||
/>
|
>
|
||||||
|
</q-btn-toggle>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@ -30,22 +31,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {defineComponent} from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import PageHeader from 'components/PageHeader.vue'
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||||
import Feed from 'components/Feed/Feed.vue'
|
import Feed from 'components/Feed/Feed.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {EventKind} from 'src/nostr/model/Event'
|
import { EventKind } from 'src/nostr/model/Event'
|
||||||
import {NoteOrder, useNoteStore} from 'src/nostr/store/NoteStore'
|
import { NoteOrder, useNoteStore } from 'src/nostr/store/NoteStore'
|
||||||
|
|
||||||
const ZERO_PUBKEY = '0000000000000000000000000000000000000000000000000000000000000000'
|
const ZERO_PUBKEY =
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
|
||||||
const myContacts = () => {
|
const myContacts = () => {
|
||||||
const app = useAppStore()
|
const app = useAppStore()
|
||||||
const nostr = useNostrStore()
|
const nostr = useNostrStore()
|
||||||
const contacts = nostr.getContacts(app.myPubkey)
|
const contacts = nostr.getContacts(app.myPubkey)
|
||||||
return contacts?.map(contact => contact.pubkey).concat(app.myPubkey)
|
return contacts?.map((contact) => contact.pubkey).concat(app.myPubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Feeds = {
|
const Feeds = {
|
||||||
@ -74,7 +76,9 @@ const Feeds = {
|
|||||||
let notes = []
|
let notes = []
|
||||||
const store = useNoteStore()
|
const store = useNoteStore()
|
||||||
for (const author of authors) {
|
for (const author of authors) {
|
||||||
notes = notes.concat(store.postsByAuthor(author, NoteOrder.CREATION_DATE_DESC))
|
notes = notes.concat(
|
||||||
|
store.postsByAuthor(author, NoteOrder.CREATION_DATE_DESC)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return notes
|
return notes
|
||||||
},
|
},
|
||||||
@ -122,15 +126,14 @@ export default defineComponent({
|
|||||||
if (this.initialized) return
|
if (this.initialized) return
|
||||||
if (!this.contacts) return
|
if (!this.contacts) return
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
this.activeFeed = this.contacts?.length
|
this.activeFeed = this.contacts?.length ? 'following' : 'global'
|
||||||
? 'following'
|
|
||||||
: 'global'
|
|
||||||
},
|
},
|
||||||
onFeedLoaded(feed) {
|
onFeedLoaded(feed) {
|
||||||
if (this.activeFeed === 'following'
|
if (
|
||||||
&& feed?.name === this.activeFeed
|
this.activeFeed === 'following' &&
|
||||||
&& !this.contacts?.length
|
feed?.name === this.activeFeed &&
|
||||||
&& !this.initialized
|
!this.contacts?.length &&
|
||||||
|
!this.initialized
|
||||||
) {
|
) {
|
||||||
this.activeFeed = 'global'
|
this.activeFeed = 'global'
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
@ -145,7 +148,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initFeed()
|
this.initFeed()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@
|
|||||||
:connector="ancestors?.length > 0"
|
:connector="ancestors?.length > 0"
|
||||||
/>
|
/>
|
||||||
<div v-else style="padding-left: 1.5rem">
|
<div v-else style="padding-left: 1.5rem">
|
||||||
<q-spinner size="sm" style="margin-right: .5rem"/> Loading...
|
<q-spinner size="sm" style="margin-right: 0.5rem" />
|
||||||
|
{{ $t("Loading...") }}
|
||||||
</div>
|
</div>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@ -28,18 +29,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="min-height: 80vh;" />
|
<div style="min-height: 80vh" />
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {defineComponent} from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import PageHeader from 'components/PageHeader.vue'
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
import Thread from 'components/Post/Thread.vue'
|
import Thread from 'components/Post/Thread.vue'
|
||||||
import HeroPost from 'components/Post/HeroPost.vue'
|
import HeroPost from 'components/Post/HeroPost.vue'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {NoteOrder} from 'src/nostr/store/NoteStore'
|
import { NoteOrder } from 'src/nostr/store/NoteStore'
|
||||||
import {bech32ToHex} from 'src/utils/utils'
|
import { bech32ToHex } from 'src/utils/utils'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ThreadPage',
|
name: 'ThreadPage',
|
||||||
@ -50,7 +51,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
nostr: useNostrStore()
|
nostr: useNostrStore(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -72,9 +73,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
rootId() {
|
rootId() {
|
||||||
if (!this.noteLoaded) return
|
if (!this.noteLoaded) return
|
||||||
return this.note.hasAncestor()
|
return this.note.hasAncestor() ? this.note.root() : this.note.id
|
||||||
? this.note.root()
|
|
||||||
: this.note.id
|
|
||||||
},
|
},
|
||||||
root() {
|
root() {
|
||||||
if (!this.rootId) return
|
if (!this.rootId) return
|
||||||
@ -89,7 +88,9 @@ export default defineComponent({
|
|||||||
const ancestors = this.allAncestors(this.note)
|
const ancestors = this.allAncestors(this.note)
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (ancestors.length > 0 && ancestors[0].id !== this.rootId) {
|
if (ancestors.length > 0 && ancestors[0].id !== this.rootId) {
|
||||||
console.error(`Invalid thread structure: expected root ${this.rootId} but found ${ancestors[0].id}`)
|
console.error(
|
||||||
|
`Invalid thread structure: expected root ${this.rootId} but found ${ancestors[0].id}`
|
||||||
|
)
|
||||||
// return
|
// return
|
||||||
}
|
}
|
||||||
return this.collectPredecessors(ancestors, this.note)
|
return this.collectPredecessors(ancestors, this.note)
|
||||||
@ -118,13 +119,14 @@ export default defineComponent({
|
|||||||
if (!ancestors || !ancestors.length) return []
|
if (!ancestors || !ancestors.length) return []
|
||||||
|
|
||||||
const ancestor = ancestors.pop()
|
const ancestor = ancestors.pop()
|
||||||
const replies = this.nostr.getRepliesTo(ancestor.id, NoteOrder.CREATION_DATE_ASC)
|
const replies = this.nostr.getRepliesTo(
|
||||||
const targetIdx = replies.findIndex(reply => reply.id === target.id)
|
ancestor.id,
|
||||||
|
NoteOrder.CREATION_DATE_ASC
|
||||||
|
)
|
||||||
|
const targetIdx = replies.findIndex((reply) => reply.id === target.id)
|
||||||
const predecessors = [ancestor].concat(replies.slice(0, targetIdx))
|
const predecessors = [ancestor].concat(replies.slice(0, targetIdx))
|
||||||
|
|
||||||
return this
|
return this.collectPredecessors(ancestors, ancestor).concat(predecessors)
|
||||||
.collectPredecessors(ancestors, ancestor)
|
|
||||||
.concat(predecessors)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
collectChildren(target, ancestor) {
|
collectChildren(target, ancestor) {
|
||||||
@ -132,8 +134,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
// Get same-level successors
|
// Get same-level successors
|
||||||
if (ancestor) {
|
if (ancestor) {
|
||||||
const ancestorReplies = this.nostr.getRepliesTo(ancestor.id, NoteOrder.CREATION_DATE_ASC)
|
const ancestorReplies = this.nostr.getRepliesTo(
|
||||||
const targetIdx = ancestorReplies.findIndex(reply => reply.id === target.id)
|
ancestor.id,
|
||||||
|
NoteOrder.CREATION_DATE_ASC
|
||||||
|
)
|
||||||
|
const targetIdx = ancestorReplies.findIndex(
|
||||||
|
(reply) => reply.id === target.id
|
||||||
|
)
|
||||||
const successors = ancestorReplies.slice(targetIdx + 1)
|
const successors = ancestorReplies.slice(targetIdx + 1)
|
||||||
if (successors.length) {
|
if (successors.length) {
|
||||||
children.push(successors)
|
children.push(successors)
|
||||||
@ -141,7 +148,10 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get children of target
|
// Get children of target
|
||||||
const targetReplies = this.nostr.getRepliesTo(target.id, NoteOrder.CREATION_DATE_DESC)
|
const targetReplies = this.nostr.getRepliesTo(
|
||||||
|
target.id,
|
||||||
|
NoteOrder.CREATION_DATE_DESC
|
||||||
|
)
|
||||||
for (const reply of targetReplies) {
|
for (const reply of targetReplies) {
|
||||||
children.push(this.unrollLongest(reply))
|
children.push(this.unrollLongest(reply))
|
||||||
}
|
}
|
||||||
@ -152,7 +162,10 @@ export default defineComponent({
|
|||||||
// Unrolls linear replies until first "fork"
|
// Unrolls linear replies until first "fork"
|
||||||
unrollLinear(root) {
|
unrollLinear(root) {
|
||||||
const thread = [root]
|
const thread = [root]
|
||||||
let replies = this.nostr.getRepliesTo(root.id, NoteOrder.CREATION_DATE_ASC)
|
let replies = this.nostr.getRepliesTo(
|
||||||
|
root.id,
|
||||||
|
NoteOrder.CREATION_DATE_ASC
|
||||||
|
)
|
||||||
while (replies.length === 1) {
|
while (replies.length === 1) {
|
||||||
thread.push(replies[0])
|
thread.push(replies[0])
|
||||||
root = replies[0]
|
root = replies[0]
|
||||||
@ -164,7 +177,10 @@ export default defineComponent({
|
|||||||
// Unrolls the longest thread in the subtree
|
// Unrolls the longest thread in the subtree
|
||||||
unrollLongest(root) {
|
unrollLongest(root) {
|
||||||
let threads = []
|
let threads = []
|
||||||
let replies = this.nostr.getRepliesTo(root.id, NoteOrder.CREATION_DATE_ASC)
|
let replies = this.nostr.getRepliesTo(
|
||||||
|
root.id,
|
||||||
|
NoteOrder.CREATION_DATE_ASC
|
||||||
|
)
|
||||||
for (const reply of replies) {
|
for (const reply of replies) {
|
||||||
threads.push(this.unrollLongest(reply))
|
threads.push(this.unrollLongest(reply))
|
||||||
}
|
}
|
||||||
@ -205,7 +221,7 @@ export default defineComponent({
|
|||||||
if (this.rootLoaded) {
|
if (this.rootLoaded) {
|
||||||
this.startStream()
|
this.startStream()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.startStream()
|
this.startStream()
|
||||||
@ -217,7 +233,7 @@ export default defineComponent({
|
|||||||
unmounted() {
|
unmounted() {
|
||||||
this.closeStream()
|
this.closeStream()
|
||||||
this.resizeObserver.disconnect()
|
this.resizeObserver.disconnect()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
/>
|
/>
|
||||||
<p v-if="!conversation?.length" class="placeholder">
|
<p v-if="!conversation?.length" class="placeholder">
|
||||||
<template v-if="counterparty !== app.myPubkey">
|
<template v-if="counterparty !== app.myPubkey">
|
||||||
This is the beginning of your message history with <UserName :pubkey="counterparty" clickable />.
|
{{ $t("This is the beginning of your message history with") }}
|
||||||
|
<UserName :pubkey="counterparty" clickable />.
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Keep private notes by sending messages to yourself.
|
Keep private notes by sending messages to yourself.
|
||||||
@ -21,7 +22,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="conversation-reply">
|
<div class="conversation-reply">
|
||||||
<MessageEditor :recipient="counterparty" :placeholder="placeholder" @publish="onPublish" autofocus />
|
<MessageEditor
|
||||||
|
:recipient="counterparty"
|
||||||
|
:placeholder="$t(placeholder)"
|
||||||
|
@publish="onPublish"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -31,13 +37,13 @@ import UserCard from 'components/User/UserCard.vue'
|
|||||||
import MessageEditor from 'components/Message/MessageEditor.vue'
|
import MessageEditor from 'components/Message/MessageEditor.vue'
|
||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
import ChatMessage from 'components/Message/ChatMessage.vue'
|
import ChatMessage from 'components/Message/ChatMessage.vue'
|
||||||
import {useMessageStore} from 'src/nostr/store/MessageStore'
|
import { useMessageStore } from 'src/nostr/store/MessageStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {bech32ToHex} from 'src/utils/utils'
|
import { bech32ToHex } from 'src/utils/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Conversation',
|
name: 'Conversation',
|
||||||
components: {ChatMessage, UserName, MessageEditor, PageHeader, UserCard},
|
components: { ChatMessage, UserName, MessageEditor, PageHeader, UserCard },
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
app: useAppStore(),
|
app: useAppStore(),
|
||||||
@ -50,10 +56,12 @@ export default {
|
|||||||
},
|
},
|
||||||
conversation() {
|
conversation() {
|
||||||
if (!this.app.isSignedIn) return
|
if (!this.app.isSignedIn) return
|
||||||
return this.messages.getConversation(this.app.myPubkey, this.counterparty)
|
return this.messages.getConversation(
|
||||||
|
this.app.myPubkey,
|
||||||
|
this.counterparty
|
||||||
|
)
|
||||||
},
|
},
|
||||||
placeholder() {
|
placeholder() {
|
||||||
// TODO i18n
|
|
||||||
return this.app.myPubkey === this.counterparty
|
return this.app.myPubkey === this.counterparty
|
||||||
? 'Jot something down'
|
? 'Jot something down'
|
||||||
: 'Message'
|
: 'Message'
|
||||||
@ -81,7 +89,7 @@ export default {
|
|||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.resizeObserver.disconnect()
|
this.resizeObserver.disconnect()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -107,7 +115,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.conversation-reply {
|
.conversation-reply {
|
||||||
background: linear-gradient(to bottom, rgba($color: $color-bg, $alpha: 0), rgba($color: $color-bg, $alpha: 1) 6%);
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color: $color-bg, $alpha: 0),
|
||||||
|
rgba($color: $color-bg, $alpha: 1) 6%
|
||||||
|
);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 600;
|
z-index: 600;
|
||||||
@ -140,5 +152,4 @@ export default {
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,24 +2,39 @@
|
|||||||
<PageHeader back-button>
|
<PageHeader back-button>
|
||||||
<template #addon>
|
<template #addon>
|
||||||
<q-btn icon="more_vert" size="md" round flat>
|
<q-btn icon="more_vert" size="md" round flat>
|
||||||
<q-menu anchor="bottom right" self="top right" :offset="[0, 6]" class="options-popup">
|
<q-menu
|
||||||
<a @click="markAllAsRead" v-close-popup>Mark all as read</a>
|
anchor="bottom right"
|
||||||
|
self="top right"
|
||||||
|
:offset="[0, 6]"
|
||||||
|
class="options-popup"
|
||||||
|
>
|
||||||
|
<a @click="markAllAsRead" v-close-popup>{{
|
||||||
|
$t("Mark all as read")
|
||||||
|
}}</a>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
<ConversationItem v-for="conversation in conversations" :key="conversation.pubkey" :conversation="conversation" />
|
<ConversationItem
|
||||||
<p v-if="!conversations?.length">To send a message, click on the <BaseIcon icon="messages" /> icon in the recipient's profile.</p>
|
v-for="conversation in conversations"
|
||||||
|
:key="conversation.pubkey"
|
||||||
|
:conversation="conversation"
|
||||||
|
/>
|
||||||
|
<p v-if="!conversations?.length">
|
||||||
|
{{ $t("To send a message, click on the") }}
|
||||||
|
<BaseIcon icon="messages" />
|
||||||
|
{{ $t("icon in the recipient's profile.") }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PageHeader from 'components/PageHeader.vue'
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {useMessageStore} from 'src/nostr/store/MessageStore'
|
import { useMessageStore } from 'src/nostr/store/MessageStore'
|
||||||
import ConversationItem from 'components/Message/ConversationItem.vue'
|
import ConversationItem from 'components/Message/ConversationItem.vue'
|
||||||
import BaseIcon from 'components/BaseIcon/index.vue'
|
import BaseIcon from 'components/BaseIcon/index.vue'
|
||||||
|
|
||||||
@ -47,7 +62,7 @@ export default {
|
|||||||
markAllAsRead() {
|
markAllAsRead() {
|
||||||
this.messages.markAllAsRead(this.app.myPubkey)
|
this.messages.markAllAsRead(this.app.myPubkey)
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -76,10 +91,10 @@ p {
|
|||||||
.options-popup {
|
.options-popup {
|
||||||
background-color: $color-bg;
|
background-color: $color-bg;
|
||||||
box-shadow: $shadow-white;
|
box-shadow: $shadow-white;
|
||||||
border-radius: .5rem;
|
border-radius: 0.5rem;
|
||||||
a {
|
a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: .5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
transition: 120ms ease;
|
transition: 120ms ease;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.1);
|
background-color: rgba($color: $color-dark-gray, $alpha: 0.1);
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
indicator-color="primary"
|
indicator-color="primary"
|
||||||
:breakpoint="0"
|
:breakpoint="0"
|
||||||
>
|
>
|
||||||
<q-tab name="following" label="Following" />
|
<q-tab name="following" :label="$t('Following')" />
|
||||||
<q-tab name="followers" label="Followers" />
|
<q-tab name="followers" :label="$t('Followers')" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -41,12 +41,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {defineComponent} from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import PageHeader from 'components/PageHeader.vue'
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
import UserCard from 'components/User/UserCard.vue'
|
import UserCard from 'components/User/UserCard.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {bech32ToHex, hexToBech32} from 'src/utils/utils'
|
import { bech32ToHex, hexToBech32 } from 'src/utils/utils'
|
||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -91,11 +91,11 @@ export default defineComponent({
|
|||||||
activeTab() {
|
activeTab() {
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
params: {
|
params: {
|
||||||
tab: this.activeTab
|
tab: this.activeTab,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -133,7 +133,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.profile-header-content .username {
|
.profile-header-content .username {
|
||||||
.name, .pubkey:first-child {
|
.name,
|
||||||
|
.pubkey:first-child {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,20 +12,23 @@
|
|||||||
<p class="about">{{ profile?.about }}</p>
|
<p class="about">{{ profile?.about }}</p>
|
||||||
<p class="followers">
|
<p class="followers">
|
||||||
<a @click="goToFollowers('following')">
|
<a @click="goToFollowers('following')">
|
||||||
<strong>{{ contacts?.length || 0 }}</strong> Following
|
<strong>{{ contacts?.length || 0 }}</strong> {{ $t("Following") }}
|
||||||
</a>
|
</a>
|
||||||
<a @click="goToFollowers('followers')">
|
<a @click="goToFollowers('followers')">
|
||||||
<strong>{{ followers?.length ? `${followers?.length}+` : 0 }}</strong> Followers
|
<strong>{{
|
||||||
|
followers?.length ? `${followers?.length}+` : 0
|
||||||
|
}}</strong>
|
||||||
|
{{ $t("Followers") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<a @click="goToConversation">
|
<a @click="goToConversation">
|
||||||
<BaseIcon icon="messages" />
|
<BaseIcon icon="messages" />
|
||||||
<q-tooltip>Send private message</q-tooltip>
|
<q-tooltip>{{ $t("Send private message") }}</q-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a :href="lightningLink" :class="{disabled: !lightningLink}">
|
<a :href="lightningLink" :class="{ disabled: !lightningLink }">
|
||||||
<q-icon name="bolt" size="sm" />
|
<q-icon name="bolt" size="sm" />
|
||||||
<q-tooltip>Tip with Bitcoin Lightning</q-tooltip>
|
<q-tooltip>{{ $t("Tip with Bitcoin Lightning") }}</q-tooltip>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -39,10 +42,10 @@
|
|||||||
indicator-color="primary"
|
indicator-color="primary"
|
||||||
:breakpoint="0"
|
:breakpoint="0"
|
||||||
>
|
>
|
||||||
<q-tab name="posts" label="Posts" />
|
<q-tab name="posts" :label="$t('Posts')" />
|
||||||
<q-tab name="replies" label="Replies" />
|
<q-tab name="replies" :label="$t('Replies')" />
|
||||||
<q-tab name="reactions" label="Reactions" />
|
<q-tab name="reactions" :label="$t('Reactions')" />
|
||||||
<q-tab name="relays" label="Relays" />
|
<q-tab name="relays" :label="$t('Relays')" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -61,23 +64,21 @@
|
|||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="replies" class="no-padding">
|
<q-tab-panel name="replies" class="no-padding">
|
||||||
<template v-for="(thread, i) in replies">
|
<template v-for="(thread, i) in replies">
|
||||||
<Thread
|
<Thread v-if="defer(i)" :key="thread[1].id" :thread="thread" />
|
||||||
v-if="defer(i)"
|
|
||||||
:key="thread[1].id"
|
|
||||||
:thread="thread"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<AsyncLoadLink :load-fn="loadMorePosts" :has-items="!!replies?.length" />
|
<AsyncLoadLink
|
||||||
|
:load-fn="loadMorePosts"
|
||||||
|
:has-items="!!replies?.length"
|
||||||
|
/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="reactions" class="no-padding">
|
<q-tab-panel name="reactions" class="no-padding">
|
||||||
<template v-for="(thread, i) in reactions">
|
<template v-for="(thread, i) in reactions">
|
||||||
<Thread
|
<Thread v-if="defer(i)" :key="thread[1].id" :thread="thread" />
|
||||||
v-if="defer(i)"
|
|
||||||
:key="thread[1].id"
|
|
||||||
:thread="thread"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<AsyncLoadLink :load-fn="loadMoreReactions" :has-items="!!reactions?.length" />
|
<AsyncLoadLink
|
||||||
|
:load-fn="loadMoreReactions"
|
||||||
|
:has-items="!!reactions?.length"
|
||||||
|
/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="relays" class="no-padding">
|
<q-tab-panel name="relays" class="no-padding">
|
||||||
<ListPlaceholder :count="0" />
|
<ListPlaceholder :count="0" />
|
||||||
@ -87,7 +88,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {defineComponent} from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import PageHeader from 'components/PageHeader.vue'
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
@ -97,11 +98,11 @@ import ListPlaceholder from 'components/ListPlaceholder.vue'
|
|||||||
import FollowButton from 'components/User/FollowButton.vue'
|
import FollowButton from 'components/User/FollowButton.vue'
|
||||||
import BaseIcon from 'components/BaseIcon/index.vue'
|
import BaseIcon from 'components/BaseIcon/index.vue'
|
||||||
import AsyncLoadLink from 'components/AsyncLoadLink.vue'
|
import AsyncLoadLink from 'components/AsyncLoadLink.vue'
|
||||||
import {useAppStore} from 'stores/App'
|
import { useAppStore } from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import { useNostrStore } from 'src/nostr/NostrStore'
|
||||||
import {bech32ToHex, hexToBech32} from 'src/utils/utils'
|
import { bech32ToHex, hexToBech32 } from 'src/utils/utils'
|
||||||
import Defer from 'src/utils/Defer'
|
import Defer from 'src/utils/Defer'
|
||||||
import {EventKind} from 'src/nostr/model/Event'
|
import { EventKind } from 'src/nostr/model/Event'
|
||||||
import DateUtils from 'src/utils/DateUtils'
|
import DateUtils from 'src/utils/DateUtils'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -143,16 +144,18 @@ export default defineComponent({
|
|||||||
return this.nostr.getPostsByAuthor(this.pubkey)
|
return this.nostr.getPostsByAuthor(this.pubkey)
|
||||||
},
|
},
|
||||||
posts() {
|
posts() {
|
||||||
return this.notes?.filter(note => !note.hasAncestor())
|
return this.notes?.filter((note) => !note.hasAncestor())
|
||||||
},
|
},
|
||||||
replies() {
|
replies() {
|
||||||
return this.notes?.filter(note => note.hasAncestor())
|
return this.notes
|
||||||
.map(note => [this.nostr.getNote(note.ancestor()), note])
|
?.filter((note) => note.hasAncestor())
|
||||||
|
.map((note) => [this.nostr.getNote(note.ancestor()), note])
|
||||||
.slice(0, 50)
|
.slice(0, 50)
|
||||||
},
|
},
|
||||||
reactions() {
|
reactions() {
|
||||||
return this.nostr.getReactionsByAuthor(this.pubkey)
|
return this.nostr
|
||||||
.map(note => [this.nostr.getNote(note.ancestor()), note])
|
.getReactionsByAuthor(this.pubkey)
|
||||||
|
.map((note) => [this.nostr.getNote(note.ancestor()), note])
|
||||||
.slice(0, 50)
|
.slice(0, 50)
|
||||||
},
|
},
|
||||||
relays() {
|
relays() {
|
||||||
@ -174,7 +177,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadMorePosts() {
|
loadMorePosts() {
|
||||||
const oldest = this.notes?.[this.notes.length - 1]?.createdAt || DateUtils.now()
|
const oldest =
|
||||||
|
this.notes?.[this.notes.length - 1]?.createdAt || DateUtils.now()
|
||||||
return this.nostr.fetch({
|
return this.nostr.fetch({
|
||||||
kinds: [EventKind.NOTE],
|
kinds: [EventKind.NOTE],
|
||||||
authors: [this.pubkey],
|
authors: [this.pubkey],
|
||||||
@ -183,7 +187,9 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadMoreReactions() {
|
loadMoreReactions() {
|
||||||
const oldest = this.reactions?.[this.reactions.length - 1]?.createdAt || DateUtils.now()
|
const oldest =
|
||||||
|
this.reactions?.[this.reactions.length - 1]?.createdAt ||
|
||||||
|
DateUtils.now()
|
||||||
return this.nostr.fetch({
|
return this.nostr.fetch({
|
||||||
kinds: [EventKind.REACTION],
|
kinds: [EventKind.REACTION],
|
||||||
authors: [this.pubkey],
|
authors: [this.pubkey],
|
||||||
@ -197,7 +203,7 @@ export default defineComponent({
|
|||||||
params: {
|
params: {
|
||||||
pubkey: hexToBech32(this.pubkey, 'npub'),
|
pubkey: hexToBech32(this.pubkey, 'npub'),
|
||||||
tab,
|
tab,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
goToConversation() {
|
goToConversation() {
|
||||||
@ -205,7 +211,7 @@ export default defineComponent({
|
|||||||
name: 'conversation',
|
name: 'conversation',
|
||||||
params: {
|
params: {
|
||||||
pubkey: hexToBech32(this.pubkey, 'npub'),
|
pubkey: hexToBech32(this.pubkey, 'npub'),
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -213,16 +219,18 @@ export default defineComponent({
|
|||||||
activeTab() {
|
activeTab() {
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
params: {
|
params: {
|
||||||
tab: this.activeTab
|
tab: this.activeTab,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.nostr.fetchPostsByAuthor(this.pubkey, 50)
|
this.nostr
|
||||||
.then(() => this.loadingNotes = false)
|
.fetchPostsByAuthor(this.pubkey, 50)
|
||||||
this.nostr.fetchReactionsByAuthor(this.pubkey, 50)
|
.then(() => (this.loadingNotes = false))
|
||||||
.then(() => this.loadingReactions = false)
|
this.nostr
|
||||||
|
.fetchReactionsByAuthor(this.pubkey, 50)
|
||||||
|
.then(() => (this.loadingReactions = false))
|
||||||
this.nostr.fetchFollowers(this.pubkey, 1000)
|
this.nostr.fetchFollowers(this.pubkey, 1000)
|
||||||
|
|
||||||
// FIXME
|
// FIXME
|
||||||
@ -230,7 +238,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (this.stream) this.nostr.cancelStream(this.stream)
|
if (this.stream) this.nostr.cancelStream(this.stream)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -262,7 +270,8 @@ export default defineComponent({
|
|||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
&:hover, &:active {
|
&:hover,
|
||||||
|
&:active {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
strong {
|
strong {
|
||||||
@ -277,7 +286,8 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
svg, i {
|
svg,
|
||||||
|
i {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
@ -285,20 +295,22 @@ export default defineComponent({
|
|||||||
transition: 120ms ease;
|
transition: 120ms ease;
|
||||||
}
|
}
|
||||||
&.disabled {
|
&.disabled {
|
||||||
svg, i {
|
svg,
|
||||||
|
i {
|
||||||
color: $color-dark-gray !important;
|
color: $color-dark-gray !important;
|
||||||
fill: $color-dark-gray !important;
|
fill: $color-dark-gray !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
svg, i {
|
svg,
|
||||||
|
i {
|
||||||
fill: $color-fg;
|
fill: $color-fg;
|
||||||
color: $color-fg;
|
color: $color-fg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a + a {
|
a + a {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import moment from 'moment/moment'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
import { es, enUS } from 'date-fns/locale'
|
||||||
|
import { $t } from '../boot/i18n'
|
||||||
|
|
||||||
// TODO i18n
|
// TODO i18n
|
||||||
const MONTHS = [
|
const MONTHS = [
|
||||||
@ -13,9 +15,11 @@ const MONTHS = [
|
|||||||
'September',
|
'September',
|
||||||
'October',
|
'October',
|
||||||
'November',
|
'November',
|
||||||
'December'
|
'December',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const [lng = 'en'] = (navigator?.language || '').split('-')
|
||||||
|
|
||||||
export default class DateUtils {
|
export default class DateUtils {
|
||||||
static now() {
|
static now() {
|
||||||
return Math.floor(Date.now() / 1000)
|
return Math.floor(Date.now() / 1000)
|
||||||
@ -23,9 +27,9 @@ export default class DateUtils {
|
|||||||
|
|
||||||
static formatDate(timestamp) {
|
static formatDate(timestamp) {
|
||||||
const date = new Date(timestamp * 1000)
|
const date = new Date(timestamp * 1000)
|
||||||
const month = MONTHS[date.getMonth()] // TODO i18n
|
const month = $t(MONTHS[date.getMonth()]) // TODO i18n
|
||||||
|
|
||||||
const sameYear = date.getFullYear() === (new Date().getFullYear())
|
const sameYear = date.getFullYear() === new Date().getFullYear()
|
||||||
const year = !sameYear ? ' ' + date.getFullYear() : ''
|
const year = !sameYear ? ' ' + date.getFullYear() : ''
|
||||||
|
|
||||||
return `${date.getDate()} ${month}${year}`
|
return `${date.getDate()} ${month}${year}`
|
||||||
@ -39,7 +43,9 @@ export default class DateUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static formatDateTime(timestamp) {
|
static formatDateTime(timestamp) {
|
||||||
return `${DateUtils.formatDate(timestamp)}, ${DateUtils.formatTime(timestamp)}`
|
return `${DateUtils.formatDate(timestamp)}, ${DateUtils.formatTime(
|
||||||
|
timestamp
|
||||||
|
)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
static formatFromNow(timestamp, format = 'long') {
|
static formatFromNow(timestamp, format = 'long') {
|
||||||
@ -49,18 +55,22 @@ export default class DateUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static formatFromNowLong(timestamp) {
|
static formatFromNowLong(timestamp) {
|
||||||
return moment(timestamp * 1000).fromNow()
|
return formatDistanceToNow(timestamp * 1000, {
|
||||||
|
locale: lng === 'es' ? es : enUS,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static formatFromNowShort(timestamp) {
|
static formatFromNowShort(timestamp) {
|
||||||
const diff = Math.max(DateUtils.now() - timestamp, 0)
|
const diff = Math.max(DateUtils.now() - timestamp, 0)
|
||||||
const formatDiff = (unit, factor, offset) => Math.max(Math.floor((diff + (unit * offset)) / (unit * factor)), 1)
|
const formatDiff = (unit, factor, offset) =>
|
||||||
|
Math.max(Math.floor((diff + unit * offset) / (unit * factor)), 1)
|
||||||
|
|
||||||
if (diff < 45) return `${formatDiff(1, 1, 0)}s`
|
if (diff < 45) return `${formatDiff(1, 1, 0)}s`
|
||||||
if (diff < 60 * 45) return `${formatDiff(1, 60, 15)}m`
|
if (diff < 60 * 45) return `${formatDiff(1, 60, 15)}m`
|
||||||
if (diff < 60 * 60 * 22) return `${formatDiff(60, 60, 15)}h`
|
if (diff < 60 * 60 * 22) return `${formatDiff(60, 60, 15)}h`
|
||||||
if (diff < 60 * 60 * 24 * 26) return `${formatDiff(60 * 60, 24, 2)}d`
|
if (diff < 60 * 60 * 24 * 26) return `${formatDiff(60 * 60, 24, 2)}d`
|
||||||
if (diff < 60 * 60 * 24 * 30 * 320) return `${formatDiff(60 * 60 * 24, 30, 4)}mo`
|
if (diff < 60 * 60 * 24 * 30 * 320)
|
||||||
|
return `${formatDiff(60 * 60 * 24, 30, 4)}mo`
|
||||||
return `${formatDiff(60 * 60 * 24, 30 * 365, 45)}y`
|
return `${formatDiff(60 * 60 * 24, 30 * 365, 45)}y`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -2618,6 +2618,11 @@ csstype@^2.6.8:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
|
||||||
integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
|
integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
|
||||||
|
|
||||||
|
date-fns@^2.29.3:
|
||||||
|
version "2.29.3"
|
||||||
|
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
|
||||||
|
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
@ -4316,11 +4321,6 @@ minimist@^1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||||
|
|
||||||
moment@^2.29.4:
|
|
||||||
version "2.29.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
|
||||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
|
||||||
|
|
||||||
mrmime@^1.0.0:
|
mrmime@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
|
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
|
||||||
|
Loading…
Reference in New Issue
Block a user