Merge branch 'develop' into refactor/notification_settings
This commit is contained in:
commit
da94935aaa
116 changed files with 4446 additions and 544 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -19,6 +19,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- 'Copy link' button for statuses (in the ellipsis menu)
|
- 'Copy link' button for statuses (in the ellipsis menu)
|
||||||
- Autocomplete domains from list of known instances
|
- Autocomplete domains from list of known instances
|
||||||
- 'Bot' settings option and badge
|
- 'Bot' settings option and badge
|
||||||
|
- Added profile meta data fields that can be set in profile settings
|
||||||
|
- Added option to reset avatar/banner in profile settings
|
||||||
|
- Descriptions can be set on uploaded files before posting
|
||||||
|
- Added status preview option to preview your statuses before posting
|
||||||
|
- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Registration page no longer requires email if the server is configured not to require it
|
- Registration page no longer requires email if the server is configured not to require it
|
||||||
|
@ -26,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Close the media modal on navigation events
|
- Close the media modal on navigation events
|
||||||
- Add colons to the emoji alt text, to make them copyable
|
- Add colons to the emoji alt text, to make them copyable
|
||||||
- Add better visual indication for drag-and-drop for files
|
- Add better visual indication for drag-and-drop for files
|
||||||
|
- When disabling attachments, the placeholder links now show an icon and the description instead of just IMAGE or VIDEO etc
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Custom Emoji will display in poll options now.
|
- Custom Emoji will display in poll options now.
|
||||||
|
@ -37,6 +43,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Subject field now appears disabled when posting
|
- Subject field now appears disabled when posting
|
||||||
- Fix status ellipsis menu being cut off in notifications column
|
- Fix status ellipsis menu being cut off in notifications column
|
||||||
- Fixed autocomplete sometimes not returning the right user when there's already some results
|
- Fixed autocomplete sometimes not returning the right user when there's already some results
|
||||||
|
- Videos and audio and misc files show description as alt/title properly now
|
||||||
|
- Clicking on non-image/video files no longer opens an empty modal
|
||||||
|
- Audio files can now be played back in the frontend with hidden attachments
|
||||||
|
- Videos are not cropped awkwardly in the uploads section anymore
|
||||||
|
- Reply filtering options in Settings -> Filtering now work again using filtering on server
|
||||||
|
- Don't show just blank-screen when cookies are disabled
|
||||||
|
|
||||||
## [2.0.3] - 2020-05-02
|
## [2.0.3] - 2020-05-02
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -98,6 +110,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Ability to change user's email
|
- Ability to change user's email
|
||||||
- About page
|
- About page
|
||||||
- Added remote user redirect
|
- Added remote user redirect
|
||||||
|
- Bookmarks
|
||||||
### Changed
|
### Changed
|
||||||
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
>
|
>
|
||||||
> --Catbag
|
> --Catbag
|
||||||
|
|
||||||
Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
|
||||||
|
|
||||||
## Posting, reading, basic functions.
|
## Posting, reading, basic functions.
|
||||||
|
|
||||||
After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
|
After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
|
||||||
|
|
8
docs/index.md
Normal file
8
docs/index.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Introduction to Pleroma-FE
|
||||||
|
## What is Pleroma-FE?
|
||||||
|
|
||||||
|
Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
||||||
|
|
||||||
|
## How can I use it?
|
||||||
|
|
||||||
|
If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
|
@ -22,6 +22,7 @@
|
||||||
"cropperjs": "^1.4.3",
|
"cropperjs": "^1.4.3",
|
||||||
"diff": "^3.0.1",
|
"diff": "^3.0.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"parse-link-header": "^1.0.1",
|
||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
"phoenix": "^1.3.0",
|
"phoenix": "^1.3.0",
|
||||||
"portal-vue": "^2.1.4",
|
"portal-vue": "^2.1.4",
|
||||||
|
|
|
@ -13,7 +13,8 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
|
||||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||||
import { windowWidth } from './services/window_utils/window_utils'
|
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||||
|
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -32,7 +33,8 @@ export default {
|
||||||
MobileNav,
|
MobileNav,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal
|
PostStatusModal,
|
||||||
|
GlobalNoticeList
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline',
|
mobileActivePanel: 'timeline',
|
||||||
|
@ -125,10 +127,12 @@ export default {
|
||||||
},
|
},
|
||||||
updateMobileState () {
|
updateMobileState () {
|
||||||
const mobileLayout = windowWidth() <= 800
|
const mobileLayout = windowWidth() <= 800
|
||||||
|
const layoutHeight = windowHeight()
|
||||||
const changed = mobileLayout !== this.isMobileLayout
|
const changed = mobileLayout !== this.isMobileLayout
|
||||||
if (changed) {
|
if (changed) {
|
||||||
this.$store.dispatch('setMobileLayout', mobileLayout)
|
this.$store.dispatch('setMobileLayout', mobileLayout)
|
||||||
}
|
}
|
||||||
|
this.$store.dispatch('setLayoutHeight', layoutHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
55
src/App.scss
55
src/App.scss
|
@ -47,6 +47,7 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
overscroll-behavior-y: none;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-family: var(--interfaceFont, sans-serif);
|
font-family: var(--interfaceFont, sans-serif);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -319,7 +320,7 @@ option {
|
||||||
|
|
||||||
i[class*=icon-] {
|
i[class*=icon-] {
|
||||||
color: $fallback--icon;
|
color: $fallback--icon;
|
||||||
color: var(--icon, $fallback--icon)
|
color: var(--icon, $fallback--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
@ -858,6 +859,10 @@ nav {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 0.8em;
|
margin-right: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin-bottom: 7em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-multiple {
|
.select-multiple {
|
||||||
|
@ -924,3 +929,51 @@ nav {
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--panel, $fallback--fg);
|
background-color: var(--panel, $fallback--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unread-chat-count {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bolder;
|
||||||
|
font-style: normal;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.6rem;
|
||||||
|
padding: 0 0.3em;
|
||||||
|
min-width: 1.3rem;
|
||||||
|
min-height: 1.3rem;
|
||||||
|
max-height: 1.3rem;
|
||||||
|
line-height: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-layout {
|
||||||
|
// Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// Ensures the fixed position of the mobile browser bars on scroll up / down events.
|
||||||
|
// Prevents the mobile browser bars from overlapping or hiding the message posting form.
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app_bg_wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
padding-top: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="app-bg-wrapper app-container-wrapper" />
|
||||||
<div
|
<div
|
||||||
id="content"
|
id="content"
|
||||||
class="container underlay"
|
class="container underlay"
|
||||||
|
@ -112,9 +113,7 @@
|
||||||
{{ $t("login.hint") }}
|
{{ $t("login.hint") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
<router-view />
|
||||||
<router-view />
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
<media-modal />
|
<media-modal />
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,6 +127,7 @@
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
<portal-target name="modal" />
|
<portal-target name="modal" />
|
||||||
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -27,5 +27,6 @@ $fallback--tooltipRadius: 5px;
|
||||||
$fallback--avatarRadius: 4px;
|
$fallback--avatarRadius: 4px;
|
||||||
$fallback--avatarAltRadius: 10px;
|
$fallback--avatarAltRadius: 10px;
|
||||||
$fallback--attachmentRadius: 10px;
|
$fallback--attachmentRadius: 10px;
|
||||||
|
$fallback--chatMessageRadius: 10px;
|
||||||
|
|
||||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
|
|
@ -8,38 +8,72 @@ import backendInteractorService from '../services/backend_interactor_service/bac
|
||||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||||
|
|
||||||
const getStatusnetConfig = async ({ store }) => {
|
let staticInitialResults = null
|
||||||
|
|
||||||
|
const parsedInitialResults = () => {
|
||||||
|
if (!document.getElementById('initial-results')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!staticInitialResults) {
|
||||||
|
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||||
|
}
|
||||||
|
return staticInitialResults
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodeUTF8Base64 = (data) => {
|
||||||
|
const rawData = atob(data)
|
||||||
|
const array = Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
|
||||||
|
const text = new TextDecoder().decode(array)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadFetch = async (request) => {
|
||||||
|
const data = parsedInitialResults()
|
||||||
|
if (!data || !data[request]) {
|
||||||
|
return window.fetch(request)
|
||||||
|
}
|
||||||
|
const decoded = decodeUTF8Base64(data[request])
|
||||||
|
const requestData = JSON.parse(decoded)
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: () => requestData,
|
||||||
|
text: () => requestData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstanceConfig = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/api/statusnet/config.json')
|
const res = await preloadFetch('/api/v1/instance')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
|
const textlimit = data.max_toot_chars
|
||||||
|
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
|
|
||||||
|
|
||||||
// TODO: default values for this stuff, added if to not make it break on
|
|
||||||
// my dev config out of the box.
|
|
||||||
if (uploadlimit) {
|
|
||||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vapidPublicKey) {
|
if (vapidPublicKey) {
|
||||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.site.pleromafe
|
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw (res)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Could not load statusnet config, potentially fatal')
|
console.error('Could not load instance config, potentially fatal')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBackendProvidedConfig = async ({ store }) => {
|
||||||
|
try {
|
||||||
|
const res = await window.fetch('/api/pleroma/frontend_configurations')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
return data.pleroma_fe
|
||||||
|
} else {
|
||||||
|
throw (res)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +166,7 @@ const getTOS = async ({ store }) => {
|
||||||
|
|
||||||
const getInstancePanel = async ({ store }) => {
|
const getInstancePanel = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/instance/panel.html')
|
const res = await preloadFetch('/instance/panel.html')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const html = await res.text()
|
const html = await res.text()
|
||||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||||
|
@ -189,24 +223,34 @@ const getAppSecret = async ({ store }) => {
|
||||||
|
|
||||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||||
nicknames.map(nickname => store.dispatch('fetchUser', nickname))
|
|
||||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNodeInfo = async ({ store }) => {
|
const getNodeInfo = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/nodeinfo/2.0.json')
|
const res = await preloadFetch('/nodeinfo/2.0.json')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const metadata = data.metadata
|
const metadata = data.metadata
|
||||||
const features = metadata.features
|
const features = metadata.features
|
||||||
|
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||||
|
|
||||||
|
const uploadLimits = metadata.uploadLimits
|
||||||
|
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||||
|
|
||||||
|
@ -257,7 +301,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
|
|
||||||
const setConfig = async ({ store }) => {
|
const setConfig = async ({ store }) => {
|
||||||
// apiConfig, staticConfig
|
// apiConfig, staticConfig
|
||||||
const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()])
|
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||||
const apiConfig = configInfos[0]
|
const apiConfig = configInfos[0]
|
||||||
const staticConfig = configInfos[1]
|
const staticConfig = configInfos[1]
|
||||||
|
|
||||||
|
@ -280,6 +324,11 @@ const checkOAuthToken = async ({ store }) => {
|
||||||
const afterStoreSetup = async ({ store, i18n }) => {
|
const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
const width = windowWidth()
|
const width = windowWidth()
|
||||||
store.dispatch('setMobileLayout', width <= 800)
|
store.dispatch('setMobileLayout', width <= 800)
|
||||||
|
|
||||||
|
const overrides = window.___pleromafe_dev_overrides || {}
|
||||||
|
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||||
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
|
||||||
await setConfig({ store })
|
await setConfig({ store })
|
||||||
|
|
||||||
const { customTheme, customThemeSource } = store.state.config
|
const { customTheme, customThemeSource } = store.state.config
|
||||||
|
@ -299,16 +348,18 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we can try getting the server settings and logging in
|
// Now we can try getting the server settings and logging in
|
||||||
|
// Most of these are preloaded into the index.html so blocking is minimized
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
checkOAuthToken({ store }),
|
checkOAuthToken({ store }),
|
||||||
getTOS({ store }),
|
|
||||||
getInstancePanel({ store }),
|
getInstancePanel({ store }),
|
||||||
getStickers({ store }),
|
getNodeInfo({ store }),
|
||||||
getNodeInfo({ store })
|
getInstanceConfig({ store })
|
||||||
])
|
])
|
||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
|
getTOS({ store })
|
||||||
|
getStickers({ store })
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
|
|
|
@ -2,9 +2,12 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||||
|
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||||
import Interactions from 'components/interactions/interactions.vue'
|
import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||||
|
import ChatList from 'components/chat_list/chat_list.vue'
|
||||||
|
import Chat from 'components/chat/chat.vue'
|
||||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||||
import Search from 'components/search/search.vue'
|
import Search from 'components/search/search.vue'
|
||||||
import Registration from 'components/registration/registration.vue'
|
import Registration from 'components/registration/registration.vue'
|
||||||
|
@ -27,7 +30,7 @@ export default (store) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
let routes = [
|
||||||
{ name: 'root',
|
{ name: 'root',
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: _to => {
|
redirect: _to => {
|
||||||
|
@ -40,6 +43,7 @@ export default (store) => {
|
||||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||||
|
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||||
{ name: 'remote-user-profile-acct',
|
{ name: 'remote-user-profile-acct',
|
||||||
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
||||||
|
@ -60,11 +64,20 @@ export default (store) => {
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
{ name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) },
|
||||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||||
|
routes = routes.concat([
|
||||||
|
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
|
||||||
|
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { mapState } from 'vuex'
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
|
|
||||||
|
@ -27,7 +28,18 @@ const AccountActions = {
|
||||||
},
|
},
|
||||||
reportUser () {
|
reportUser () {
|
||||||
this.$store.dispatch('openUserReportingModal', this.user.id)
|
this.$store.dispatch('openUserReportingModal', this.user.id)
|
||||||
|
},
|
||||||
|
openChat () {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'chat',
|
||||||
|
params: { recipient_id: this.user.id }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,13 @@
|
||||||
>
|
>
|
||||||
{{ $t('user_card.report') }}
|
{{ $t('user_card.report') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="pleromaChatMessagesAvailable"
|
||||||
|
class="btn btn-default btn-block dropdown-item"
|
||||||
|
@click="openChat"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.message') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -8,7 +8,6 @@ const Attachment = {
|
||||||
props: [
|
props: [
|
||||||
'attachment',
|
'attachment',
|
||||||
'nsfw',
|
'nsfw',
|
||||||
'statusId',
|
|
||||||
'size',
|
'size',
|
||||||
'allowPlay',
|
'allowPlay',
|
||||||
'setMedia',
|
'setMedia',
|
||||||
|
@ -30,9 +29,21 @@ const Attachment = {
|
||||||
VideoAttachment
|
VideoAttachment
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
usePlaceHolder () {
|
usePlaceholder () {
|
||||||
return this.size === 'hide' || this.type === 'unknown'
|
return this.size === 'hide' || this.type === 'unknown'
|
||||||
},
|
},
|
||||||
|
placeholderName () {
|
||||||
|
if (this.attachment.description === '' || !this.attachment.description) {
|
||||||
|
return this.type.toUpperCase()
|
||||||
|
}
|
||||||
|
return this.attachment.description
|
||||||
|
},
|
||||||
|
placeholderIconClass () {
|
||||||
|
if (this.type === 'image') return 'icon-picture'
|
||||||
|
if (this.type === 'video') return 'icon-video'
|
||||||
|
if (this.type === 'audio') return 'icon-music'
|
||||||
|
return 'icon-doc'
|
||||||
|
},
|
||||||
referrerpolicy () {
|
referrerpolicy () {
|
||||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||||
},
|
},
|
||||||
|
@ -49,7 +60,15 @@ const Attachment = {
|
||||||
return this.size === 'small'
|
return this.size === 'small'
|
||||||
},
|
},
|
||||||
fullwidth () {
|
fullwidth () {
|
||||||
return this.type === 'html' || this.type === 'audio'
|
if (this.size === 'hide') return false
|
||||||
|
return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
|
||||||
|
},
|
||||||
|
useModal () {
|
||||||
|
const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
|
||||||
|
: this.mergedConfig.playVideosInModal
|
||||||
|
? ['image', 'video']
|
||||||
|
: ['image']
|
||||||
|
return modalTypes.includes(this.type)
|
||||||
},
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
|
@ -60,12 +79,7 @@ const Attachment = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModal (event) {
|
openModal (event) {
|
||||||
const modalTypes = this.mergedConfig.playVideosInModal
|
if (this.useModal) {
|
||||||
? ['image', 'video']
|
|
||||||
: ['image']
|
|
||||||
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
|
|
||||||
this.usePlaceHolder
|
|
||||||
) {
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.setMedia()
|
this.setMedia()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="usePlaceHolder"
|
v-if="usePlaceholder"
|
||||||
|
:class="{ 'fullwidth': fullwidth }"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
@ -8,8 +9,11 @@
|
||||||
class="placeholder"
|
class="placeholder"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
>
|
>
|
||||||
[{{ nsfw ? "NSFW/" : "" }}{{ type.toUpperCase() }}]
|
<span :class="placeholderIconClass" />
|
||||||
|
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -22,6 +26,8 @@
|
||||||
v-if="hidden"
|
v-if="hidden"
|
||||||
class="image-attachment"
|
class="image-attachment"
|
||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
@click.prevent="toggleHidden"
|
@click.prevent="toggleHidden"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -51,7 +57,6 @@
|
||||||
:class="{'hidden': hidden && preloadImage }"
|
:class="{'hidden': hidden && preloadImage }"
|
||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="attachment.description"
|
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
<StillImage
|
<StillImage
|
||||||
|
@ -59,6 +64,7 @@
|
||||||
:mimetype="attachment.mimetype"
|
:mimetype="attachment.mimetype"
|
||||||
:src="attachment.large_thumb_url || attachment.url"
|
:src="attachment.large_thumb_url || attachment.url"
|
||||||
:image-load-handler="onImageLoad"
|
:image-load-handler="onImageLoad"
|
||||||
|
:alt="attachment.description"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -83,6 +89,8 @@
|
||||||
<audio
|
<audio
|
||||||
v-if="type === 'audio'"
|
v-if="type === 'audio'"
|
||||||
:src="attachment.url"
|
:src="attachment.url"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
controls
|
controls
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -116,22 +124,19 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.attachment.media-upload-container {
|
.non-gallery {
|
||||||
flex: 0 0 auto;
|
|
||||||
max-height: 200px;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
video {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
margin-right: 8px;
|
display: inline-block;
|
||||||
margin-bottom: 4px;
|
padding: 0.3em 1em 0.3em 0;
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
color: var(--postLink, $fallback--link);
|
color: var(--postLink, $fallback--link);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nsfw-placeholder {
|
.nsfw-placeholder {
|
||||||
|
|
17
src/components/bookmark_timeline/bookmark_timeline.js
Normal file
17
src/components/bookmark_timeline/bookmark_timeline.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
|
const Bookmarks = {
|
||||||
|
computed: {
|
||||||
|
timeline () {
|
||||||
|
return this.$store.state.statuses.timelines.bookmarks
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Timeline
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bookmarks
|
9
src/components/bookmark_timeline/bookmark_timeline.vue
Normal file
9
src/components/bookmark_timeline/bookmark_timeline.vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<Timeline
|
||||||
|
:title="$t('nav.bookmarks')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'bookmarks'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./bookmark_timeline.js"></script>
|
333
src/components/chat/chat.js
Normal file
333
src/components/chat/chat.js
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||||
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
import ChatMessage from '../chat_message/chat_message.vue'
|
||||||
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
|
import ChatTitle from '../chat_title/chat_title.vue'
|
||||||
|
import chatService from '../../services/chat_service/chat_service.js'
|
||||||
|
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
|
||||||
|
|
||||||
|
const BOTTOMED_OUT_OFFSET = 10
|
||||||
|
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
||||||
|
const SAFE_RESIZE_TIME_OFFSET = 100
|
||||||
|
|
||||||
|
const Chat = {
|
||||||
|
components: {
|
||||||
|
ChatMessage,
|
||||||
|
ChatTitle,
|
||||||
|
PostStatusForm
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
jumpToBottomButtonVisible: false,
|
||||||
|
hoveredMessageChainId: undefined,
|
||||||
|
lastScrollPosition: {},
|
||||||
|
scrollableContainerHeight: '100%',
|
||||||
|
errorLoadingChat: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.startFetching()
|
||||||
|
window.addEventListener('resize', this.handleLayoutChange)
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
|
if (typeof document.hidden !== 'undefined') {
|
||||||
|
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
this.handleResize()
|
||||||
|
})
|
||||||
|
this.setChatLayout()
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
window.removeEventListener('resize', this.handleLayoutChange)
|
||||||
|
this.unsetChatLayout()
|
||||||
|
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||||
|
this.$store.dispatch('clearCurrentChat')
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
recipient () {
|
||||||
|
return this.currentChat && this.currentChat.account
|
||||||
|
},
|
||||||
|
recipientId () {
|
||||||
|
return this.$route.params.recipient_id
|
||||||
|
},
|
||||||
|
formPlaceholder () {
|
||||||
|
if (this.recipient) {
|
||||||
|
return this.$t('chats.message_user', { nickname: this.recipient.screen_name })
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chatViewItems () {
|
||||||
|
return chatService.getView(this.currentChatMessageService)
|
||||||
|
},
|
||||||
|
newMessageCount () {
|
||||||
|
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
|
||||||
|
},
|
||||||
|
streamingEnabled () {
|
||||||
|
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||||
|
},
|
||||||
|
...mapGetters([
|
||||||
|
'currentChat',
|
||||||
|
'currentChatMessageService',
|
||||||
|
'findOpenedChatByRecipientId',
|
||||||
|
'mergedConfig'
|
||||||
|
]),
|
||||||
|
...mapState({
|
||||||
|
backendInteractor: state => state.api.backendInteractor,
|
||||||
|
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
||||||
|
mobileLayout: state => state.interface.mobileLayout,
|
||||||
|
layoutHeight: state => state.interface.layoutHeight,
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
chatViewItems () {
|
||||||
|
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
||||||
|
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||||
|
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (bottomedOutBeforeUpdate) {
|
||||||
|
this.scrollDown({ forceRead: !document.hidden })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'$route': function () {
|
||||||
|
this.startFetching()
|
||||||
|
},
|
||||||
|
layoutHeight () {
|
||||||
|
this.handleResize({ expand: true })
|
||||||
|
},
|
||||||
|
mastoUserSocketStatus (newValue) {
|
||||||
|
if (newValue === WSConnectionStatus.JOINED) {
|
||||||
|
this.fetchChat({ isFirstFetch: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
||||||
|
onMessageHover ({ isHovered, messageChainId }) {
|
||||||
|
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
||||||
|
},
|
||||||
|
onFilesDropped () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleResize()
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleVisibilityChange () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||||
|
this.scrollDown({ forceRead: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setChatLayout () {
|
||||||
|
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
|
||||||
|
// This layout prevents empty spaces from being visible at the bottom
|
||||||
|
// of the chat on iOS Safari (`safe-area-inset`) when
|
||||||
|
// - the on-screen keyboard appears and the user starts typing
|
||||||
|
// - the user selects the text inside the input area
|
||||||
|
// - the user selects and deletes the text that is multiple lines long
|
||||||
|
// TODO: unify the chat layout with the global layout.
|
||||||
|
let html = document.querySelector('html')
|
||||||
|
if (html) {
|
||||||
|
html.classList.add('chat-layout')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unsetChatLayout () {
|
||||||
|
let html = document.querySelector('html')
|
||||||
|
if (html) {
|
||||||
|
html.classList.remove('chat-layout')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLayoutChange () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
this.scrollDown()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
|
||||||
|
updateScrollableContainerHeight () {
|
||||||
|
const header = this.$refs.header
|
||||||
|
const footer = this.$refs.footer
|
||||||
|
const inner = this.mobileLayout ? window.document.body : this.$refs.inner
|
||||||
|
this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
|
||||||
|
},
|
||||||
|
// Preserves the scroll position when OSK appears or the posting form changes its height.
|
||||||
|
handleResize (opts = {}) {
|
||||||
|
const { expand = false, delayed = false } = opts
|
||||||
|
|
||||||
|
if (delayed) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.handleResize({ ...opts, delayed: false })
|
||||||
|
}, SAFE_RESIZE_TIME_OFFSET)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
|
||||||
|
const { offsetHeight = undefined } = this.lastScrollPosition
|
||||||
|
this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
|
||||||
|
|
||||||
|
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
||||||
|
if (diff < 0 || (!this.bottomedOut() && expand)) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
this.$refs.scrollable.scrollTo({
|
||||||
|
top: this.$refs.scrollable.scrollTop - diff,
|
||||||
|
left: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scrollDown (options = {}) {
|
||||||
|
const { behavior = 'auto', forceRead = false } = options
|
||||||
|
const scrollable = this.$refs.scrollable
|
||||||
|
if (!scrollable) { return }
|
||||||
|
this.$nextTick(() => {
|
||||||
|
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
||||||
|
})
|
||||||
|
if (forceRead || this.newMessageCount > 0) {
|
||||||
|
this.readChat()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readChat () {
|
||||||
|
if (!(this.currentChatMessageService && this.currentChatMessageService.lastMessage)) { return }
|
||||||
|
if (document.hidden) { return }
|
||||||
|
const lastReadId = this.currentChatMessageService.lastMessage.id
|
||||||
|
this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId })
|
||||||
|
},
|
||||||
|
bottomedOut (offset) {
|
||||||
|
return isBottomedOut(this.$refs.scrollable, offset)
|
||||||
|
},
|
||||||
|
reachedTop () {
|
||||||
|
const scrollable = this.$refs.scrollable
|
||||||
|
return scrollable && scrollable.scrollTop <= 0
|
||||||
|
},
|
||||||
|
handleScroll: _.throttle(function () {
|
||||||
|
if (!this.currentChat) { return }
|
||||||
|
|
||||||
|
if (this.reachedTop()) {
|
||||||
|
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||||
|
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||||
|
this.jumpToBottomButtonVisible = false
|
||||||
|
if (this.newMessageCount > 0) {
|
||||||
|
this.readChat()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.jumpToBottomButtonVisible = true
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
|
handleScrollUp (positionBeforeLoading) {
|
||||||
|
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
||||||
|
this.$refs.scrollable.scrollTo({
|
||||||
|
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||||
|
left: 0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||||
|
const chatMessageService = this.currentChatMessageService
|
||||||
|
if (!chatMessageService) { return }
|
||||||
|
if (fetchLatest && this.streamingEnabled) { return }
|
||||||
|
|
||||||
|
const chatId = chatMessageService.chatId
|
||||||
|
const fetchOlderMessages = !!maxId
|
||||||
|
const sinceId = fetchLatest && chatMessageService.lastMessage && chatMessageService.lastMessage.id
|
||||||
|
|
||||||
|
this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
||||||
|
.then((messages) => {
|
||||||
|
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||||
|
if (isFirstFetch) {
|
||||||
|
chatService.clear(chatMessageService)
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
|
||||||
|
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (fetchOlderMessages) {
|
||||||
|
this.handleScrollUp(positionBeforeUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstFetch) {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async startFetching () {
|
||||||
|
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
||||||
|
if (!chat) {
|
||||||
|
try {
|
||||||
|
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error creating or getting a chat', e)
|
||||||
|
this.errorLoadingChat = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chat) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollDown({ forceRead: true })
|
||||||
|
})
|
||||||
|
this.$store.dispatch('addOpenedChat', { chat })
|
||||||
|
this.doStartFetching()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doStartFetching () {
|
||||||
|
this.$store.dispatch('startFetchingCurrentChat', {
|
||||||
|
fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
||||||
|
})
|
||||||
|
this.fetchChat({ isFirstFetch: true })
|
||||||
|
},
|
||||||
|
sendMessage ({ status, media }) {
|
||||||
|
const params = {
|
||||||
|
id: this.currentChat.id,
|
||||||
|
content: status
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media[0]) {
|
||||||
|
params.mediaId = media[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.backendInteractor.sendChatMessage(params)
|
||||||
|
.then(data => {
|
||||||
|
this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleResize()
|
||||||
|
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||||
|
// to account for the potential delay in the DOM update.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
}, SAFE_RESIZE_TIME_OFFSET)
|
||||||
|
this.scrollDown({ forceRead: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error sending message', error)
|
||||||
|
return {
|
||||||
|
error: this.$t('chats.error_sending_message')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goBack () {
|
||||||
|
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat
|
162
src/components/chat/chat.scss
Normal file
162
src/components/chat/chat.scss
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
.chat-view {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.chat-title {
|
||||||
|
// prevents chat header jumping on when the user avatar loads
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-inner {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
margin: 0.5em 0.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-body {
|
||||||
|
background-color: var(--chatBg, $fallback--bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-message-list {
|
||||||
|
padding: 0 0.8em;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-heading {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
top: 50px;
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
position: sticky;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-button {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 1.4em;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-bottom-button {
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
border-radius: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: 1.3em;
|
||||||
|
top: -3.2em;
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--btn, $fallback--fg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
transition: 0.35s all;
|
||||||
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 1em;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-message-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-top: -1rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-loading-error {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.chat-view-inner {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-body {
|
||||||
|
display: flex;
|
||||||
|
min-height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-heading {
|
||||||
|
position: static;
|
||||||
|
z-index: 9999;
|
||||||
|
top: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-message-list {
|
||||||
|
display: unset;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
src/components/chat/chat.vue
Normal file
100
src/components/chat/chat.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div class="chat-view">
|
||||||
|
<div class="chat-view-inner">
|
||||||
|
<div
|
||||||
|
id="nav"
|
||||||
|
ref="inner"
|
||||||
|
class="panel-default panel chat-view-body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="header"
|
||||||
|
class="panel-heading chat-view-heading mobile-hidden"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="go-back-button"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<i class="button-icon icon-left-open" />
|
||||||
|
</a>
|
||||||
|
<div class="title text-center">
|
||||||
|
<ChatTitle
|
||||||
|
:user="recipient"
|
||||||
|
:with-avatar="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="scrollable"
|
||||||
|
class="scrollable-message-list"
|
||||||
|
:style="{ height: scrollableContainerHeight }"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<template v-if="!errorLoadingChat">
|
||||||
|
<ChatMessage
|
||||||
|
v-for="chatViewItem in chatViewItems"
|
||||||
|
:key="chatViewItem.id"
|
||||||
|
:author="recipient"
|
||||||
|
:chat-view-item="chatViewItem"
|
||||||
|
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
||||||
|
@hover="onMessageHover"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="chat-loading-error"
|
||||||
|
>
|
||||||
|
<div class="alert error">
|
||||||
|
{{ $t('chats.error_loading_chat') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="footer"
|
||||||
|
class="panel-body footer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="jump-to-bottom-button"
|
||||||
|
:class="{ 'visible': jumpToBottomButtonVisible }"
|
||||||
|
@click="scrollDown({ behavior: 'smooth' })"
|
||||||
|
>
|
||||||
|
<i class="icon-down-open">
|
||||||
|
<div
|
||||||
|
v-if="newMessageCount"
|
||||||
|
class="badge badge-notification unread-chat-count unread-message-count"
|
||||||
|
>
|
||||||
|
{{ newMessageCount }}
|
||||||
|
</div>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
<PostStatusForm
|
||||||
|
:disable-subject="true"
|
||||||
|
:disable-scope-selector="true"
|
||||||
|
:disable-notice="true"
|
||||||
|
:disable-lock-warning="true"
|
||||||
|
:disable-polls="true"
|
||||||
|
:disable-sensitivity-checkbox="true"
|
||||||
|
:disable-submit="errorLoadingChat || !currentChat"
|
||||||
|
:disable-preview="true"
|
||||||
|
:post-handler="sendMessage"
|
||||||
|
:submit-on-enter="!mobileLayout"
|
||||||
|
:preserve-focus="!mobileLayout"
|
||||||
|
:auto-focus="!mobileLayout"
|
||||||
|
:placeholder="formPlaceholder"
|
||||||
|
:file-limit="1"
|
||||||
|
max-height="160"
|
||||||
|
emoji-picker-placement="top"
|
||||||
|
@resize="handleResize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import './chat.scss';
|
||||||
|
</style>
|
26
src/components/chat/chat_layout_utils.js
Normal file
26
src/components/chat/chat_layout_utils.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Captures a scroll position
|
||||||
|
export const getScrollPosition = (el) => {
|
||||||
|
return {
|
||||||
|
scrollTop: el.scrollTop,
|
||||||
|
scrollHeight: el.scrollHeight,
|
||||||
|
offsetHeight: el.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
|
||||||
|
// Takes two scroll positions, before and after the update.
|
||||||
|
export const getNewTopPosition = (previousPosition, newPosition) => {
|
||||||
|
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isBottomedOut = (el, offset = 0) => {
|
||||||
|
if (!el) { return }
|
||||||
|
const scrollHeight = el.scrollTop + offset
|
||||||
|
const totalHeight = el.scrollHeight - el.offsetHeight
|
||||||
|
return totalHeight <= scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form.
|
||||||
|
export const scrollableContainerHeight = (inner, header, footer) => {
|
||||||
|
return inner.offsetHeight - header.clientHeight - footer.clientHeight
|
||||||
|
}
|
37
src/components/chat_list/chat_list.js
Normal file
37
src/components/chat_list/chat_list.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
||||||
|
import ChatNew from '../chat_new/chat_new.vue'
|
||||||
|
import List from '../list/list.vue'
|
||||||
|
|
||||||
|
const ChatList = {
|
||||||
|
components: {
|
||||||
|
ChatListItem,
|
||||||
|
List,
|
||||||
|
ChatNew
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
...mapGetters(['sortedChatList'])
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isNew: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('fetchChats', { latest: true })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelNewChat () {
|
||||||
|
this.isNew = false
|
||||||
|
this.$store.dispatch('fetchChats', { latest: true })
|
||||||
|
},
|
||||||
|
newChat () {
|
||||||
|
this.isNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatList
|
64
src/components/chat_list/chat_list.vue
Normal file
64
src/components/chat_list/chat_list.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="isNew">
|
||||||
|
<ChatNew @cancel="cancelNewChat" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="chat-list panel panel-default"
|
||||||
|
>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="title">
|
||||||
|
{{ $t("chats.chats") }}
|
||||||
|
</span>
|
||||||
|
<button @click="newChat">
|
||||||
|
{{ $t("chats.new") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div
|
||||||
|
v-if="sortedChatList.length > 0"
|
||||||
|
class="timeline"
|
||||||
|
>
|
||||||
|
<List :items="sortedChatList">
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="{item}"
|
||||||
|
>
|
||||||
|
<ChatListItem
|
||||||
|
:key="item.id"
|
||||||
|
:compact="false"
|
||||||
|
:chat="item"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="emtpy-chat-list-alert"
|
||||||
|
>
|
||||||
|
<span>{{ $t('chats.empty_chat_list_placeholder') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat_list.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.chat-list {
|
||||||
|
min-height: 25em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emtpy-chat-list-alert {
|
||||||
|
padding: 3em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--faint, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
67
src/components/chat_list_item/chat_list_item.js
Normal file
67
src/components/chat_list_item/chat_list_item.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
import ChatTitle from '../chat_title/chat_title.vue'
|
||||||
|
|
||||||
|
const ChatListItem = {
|
||||||
|
name: 'ChatListItem',
|
||||||
|
props: [
|
||||||
|
'chat'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
UserAvatar,
|
||||||
|
AvatarList,
|
||||||
|
Timeago,
|
||||||
|
ChatTitle,
|
||||||
|
StatusContent
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
attachmentInfo () {
|
||||||
|
if (this.chat.lastMessage.attachments.length === 0) { return }
|
||||||
|
|
||||||
|
const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
|
||||||
|
if (types.includes('video')) {
|
||||||
|
return this.$t('file_type.video')
|
||||||
|
} else if (types.includes('audio')) {
|
||||||
|
return this.$t('file_type.audio')
|
||||||
|
} else if (types.includes('image')) {
|
||||||
|
return this.$t('file_type.image')
|
||||||
|
} else {
|
||||||
|
return this.$t('file_type.file')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messageForStatusContent () {
|
||||||
|
const message = this.chat.lastMessage
|
||||||
|
const isYou = message && message.account_id === this.currentUser.id
|
||||||
|
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||||
|
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||||
|
return {
|
||||||
|
summary: '',
|
||||||
|
statusnet_html: messagePreview,
|
||||||
|
text: messagePreview,
|
||||||
|
attachments: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openChat (_e) {
|
||||||
|
if (this.chat.id) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'chat',
|
||||||
|
params: {
|
||||||
|
username: this.currentUser.screen_name,
|
||||||
|
recipient_id: this.chat.account.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatListItem
|
94
src/components/chat_list_item/chat_list_item.scss
Normal file
94
src/components/chat_list_item/chat_list_item.scss
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
.chat-list-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0.75em;
|
||||||
|
height: 5em;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--selectedPost, $fallback--lightBg);
|
||||||
|
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-list-item-left {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-list-item-center {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-right {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-and-account-name {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 1;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-preview {
|
||||||
|
display: inline-flex;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin: 0.35em 0;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--faint, $fallback--text);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--faintLink, $fallback--link);
|
||||||
|
text-decoration: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .animated.avatar {
|
||||||
|
canvas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.still-image {
|
||||||
|
border-radius: $fallback--avatarAltRadius;
|
||||||
|
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-body {
|
||||||
|
img.emoji {
|
||||||
|
width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-wrapper {
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
52
src/components/chat_list_item/chat_list_item.vue
Normal file
52
src/components/chat_list_item/chat_list_item.vue
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="chat-list-item"
|
||||||
|
@click.capture.prevent="openChat"
|
||||||
|
>
|
||||||
|
<div class="chat-list-item-left">
|
||||||
|
<UserAvatar
|
||||||
|
:user="chat.account"
|
||||||
|
height="48px"
|
||||||
|
width="48px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="chat-list-item-center">
|
||||||
|
<div class="heading">
|
||||||
|
<span
|
||||||
|
v-if="chat.account"
|
||||||
|
class="name-and-account-name"
|
||||||
|
>
|
||||||
|
<ChatTitle
|
||||||
|
:user="chat.account"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="heading-right" />
|
||||||
|
</div>
|
||||||
|
<div class="chat-preview">
|
||||||
|
<StatusContent
|
||||||
|
:status="messageForStatusContent"
|
||||||
|
:single-line="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="chat.unread > 0"
|
||||||
|
class="badge badge-notification unread-chat-count"
|
||||||
|
>
|
||||||
|
{{ chat.unread }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-wrapper">
|
||||||
|
<Timeago
|
||||||
|
:time="chat.updated_at"
|
||||||
|
:auto-update="60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat_list_item.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import './chat_list_item.scss';
|
||||||
|
</style>
|
96
src/components/chat_message/chat_message.js
Normal file
96
src/components/chat_message/chat_message.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
import Attachment from '../attachment/attachment.vue'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import Gallery from '../gallery/gallery.vue'
|
||||||
|
import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
|
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
|
const ChatMessage = {
|
||||||
|
name: 'ChatMessage',
|
||||||
|
props: [
|
||||||
|
'author',
|
||||||
|
'edited',
|
||||||
|
'noHeading',
|
||||||
|
'chatViewItem',
|
||||||
|
'hoveredMessageChain'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
Popover,
|
||||||
|
Attachment,
|
||||||
|
StatusContent,
|
||||||
|
UserAvatar,
|
||||||
|
Gallery,
|
||||||
|
LinkPreview,
|
||||||
|
ChatMessageDate
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// Returns HH:MM (hours and minutes) in local time.
|
||||||
|
createdAt () {
|
||||||
|
const time = this.chatViewItem.data.created_at
|
||||||
|
return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||||
|
},
|
||||||
|
isCurrentUser () {
|
||||||
|
return this.message.account_id === this.currentUser.id
|
||||||
|
},
|
||||||
|
message () {
|
||||||
|
return this.chatViewItem.data
|
||||||
|
},
|
||||||
|
userProfileLink () {
|
||||||
|
return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
},
|
||||||
|
isMessage () {
|
||||||
|
return this.chatViewItem.type === 'message'
|
||||||
|
},
|
||||||
|
messageForStatusContent () {
|
||||||
|
return {
|
||||||
|
summary: '',
|
||||||
|
statusnet_html: this.message.content,
|
||||||
|
text: this.message.content,
|
||||||
|
attachments: this.message.attachments
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasAttachment () {
|
||||||
|
return this.message.attachments.length > 0
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
restrictedNicknames: state => state.instance.restrictedNicknames
|
||||||
|
}),
|
||||||
|
popoverMarginStyle () {
|
||||||
|
if (this.isCurrentUser) {
|
||||||
|
return {}
|
||||||
|
} else {
|
||||||
|
return { left: 50 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...mapGetters(['mergedConfig', 'findUser'])
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
hovered: false,
|
||||||
|
menuOpened: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onHover (bool) {
|
||||||
|
this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
|
||||||
|
},
|
||||||
|
async deleteMessage () {
|
||||||
|
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
|
||||||
|
if (confirmed) {
|
||||||
|
await this.$store.dispatch('deleteChatMessage', {
|
||||||
|
messageId: this.chatViewItem.data.id,
|
||||||
|
chatId: this.chatViewItem.data.chat_id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.hovered = false
|
||||||
|
this.menuOpened = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatMessage
|
164
src/components/chat_message/chat_message.scss
Normal file
164
src/components/chat_message/chat_message.scss
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.chat-message-wrapper {
|
||||||
|
&.hovered-message-chain {
|
||||||
|
.animated.avatar {
|
||||||
|
canvas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-menu {
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding-top: 0.2em;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ellipsis {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover, .extra-button-popover.open & {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: $fallback--chatMessageRadius;
|
||||||
|
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
margin-right: 0.72em;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-preview, .attachments {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 80%;
|
||||||
|
min-width: 10em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.with-media {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.gallery-row {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
border-radius: $fallback--chatMessageRadius;
|
||||||
|
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
||||||
|
display: flex;
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.created-at {
|
||||||
|
position: relative;
|
||||||
|
float: right;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin: -1em 0 -0.5em 0;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.without-attachment {
|
||||||
|
.status-content {
|
||||||
|
&::after {
|
||||||
|
margin-right: 5.4em;
|
||||||
|
content: " ";
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming {
|
||||||
|
a {
|
||||||
|
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: var(--chatMessageIncomingText, $fallback--text);
|
||||||
|
background-color: var(--chatMessageIncomingBg, $fallback--bg);
|
||||||
|
border: 1px solid var(--chatMessageIncomingBorder, --border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.created-at {
|
||||||
|
a {
|
||||||
|
color: var(--chatMessageIncomingText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-menu {
|
||||||
|
left: 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--chatMessageOutgoingLink, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: var(--chatMessageOutgoingText, $fallback--text);
|
||||||
|
background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
|
||||||
|
border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-inner {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-menu {
|
||||||
|
right: 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-date-separator {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.4em 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
user-select: none;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--faintedText, $fallback--text);
|
||||||
|
}
|
99
src/components/chat_message/chat_message.vue
Normal file
99
src/components/chat_message/chat_message.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isMessage"
|
||||||
|
class="chat-message-wrapper"
|
||||||
|
:class="{ 'hovered-message-chain': hoveredMessageChain }"
|
||||||
|
@mouseover="onHover(true)"
|
||||||
|
@mouseleave="onHover(false)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="chat-message"
|
||||||
|
:class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!isCurrentUser"
|
||||||
|
class="avatar-wrapper"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="chatViewItem.isHead"
|
||||||
|
:to="userProfileLink"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:compact="true"
|
||||||
|
:better-shadow="betterShadow"
|
||||||
|
:user="author"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="chat-message-inner">
|
||||||
|
<div
|
||||||
|
class="status-body"
|
||||||
|
:style="{ 'min-width': message.attachment ? '80%' : '' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="media status"
|
||||||
|
:class="{ 'without-attachment': !hasAttachment }"
|
||||||
|
style="position: relative"
|
||||||
|
@mouseenter="hovered = true"
|
||||||
|
@mouseleave="hovered = false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="chat-message-menu"
|
||||||
|
:class="{ 'visible': hovered || menuOpened }"
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
placement="top"
|
||||||
|
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
|
||||||
|
:bound-to="{ x: 'container' }"
|
||||||
|
:margin="popoverMarginStyle"
|
||||||
|
@show="menuOpened = true"
|
||||||
|
@close="menuOpened = false"
|
||||||
|
>
|
||||||
|
<div slot="content">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click="deleteMessage"
|
||||||
|
>
|
||||||
|
<i class="icon-cancel" /> {{ $t("chats.delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
:title="$t('chats.more')"
|
||||||
|
>
|
||||||
|
<i class="icon-ellipsis" />
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<StatusContent
|
||||||
|
:status="messageForStatusContent"
|
||||||
|
:full-content="true"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
slot="footer"
|
||||||
|
class="created-at"
|
||||||
|
>
|
||||||
|
{{ createdAt }}
|
||||||
|
</span>
|
||||||
|
</StatusContent>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="chat-message-date-separator"
|
||||||
|
>
|
||||||
|
<ChatMessageDate :date="chatViewItem.date" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat_message.js" ></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import './chat_message.scss';
|
||||||
|
|
||||||
|
</style>
|
24
src/components/chat_message_date/chat_message_date.vue
Normal file
24
src/components/chat_message_date/chat_message_date.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<time>
|
||||||
|
{{ displayDate }}
|
||||||
|
</time>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Timeago',
|
||||||
|
props: ['date'],
|
||||||
|
computed: {
|
||||||
|
displayDate () {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
if (this.date.getTime() === today.getTime()) {
|
||||||
|
return this.$t('display_date.today')
|
||||||
|
} else {
|
||||||
|
return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
73
src/components/chat_new/chat_new.js
Normal file
73
src/components/chat_new/chat_new.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
|
const chatNew = {
|
||||||
|
components: {
|
||||||
|
BasicUserCard,
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
suggestions: [],
|
||||||
|
userIds: [],
|
||||||
|
loading: false,
|
||||||
|
query: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
const { chats } = await this.backendInteractor.chats()
|
||||||
|
chats.forEach(chat => this.suggestions.push(chat.account))
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
users () {
|
||||||
|
return this.userIds.map(userId => this.findUser(userId))
|
||||||
|
},
|
||||||
|
availableUsers () {
|
||||||
|
if (this.query.length !== 0) {
|
||||||
|
return this.users
|
||||||
|
} else {
|
||||||
|
return this.suggestions
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
backendInteractor: state => state.api.backendInteractor
|
||||||
|
}),
|
||||||
|
...mapGetters(['findUser'])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack () {
|
||||||
|
this.$emit('cancel')
|
||||||
|
},
|
||||||
|
goToChat (user) {
|
||||||
|
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
||||||
|
},
|
||||||
|
onInput () {
|
||||||
|
this.search(this.query)
|
||||||
|
},
|
||||||
|
addUser (user) {
|
||||||
|
this.selectedUserIds.push(user.id)
|
||||||
|
this.query = ''
|
||||||
|
},
|
||||||
|
removeUser (userId) {
|
||||||
|
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
||||||
|
},
|
||||||
|
search (query) {
|
||||||
|
if (!query) {
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.userIds = []
|
||||||
|
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
|
||||||
|
.then(data => {
|
||||||
|
this.loading = false
|
||||||
|
this.userIds = data.accounts.map(a => a.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default chatNew
|
29
src/components/chat_new/chat_new.scss
Normal file
29
src/components/chat_new/chat_new.scss
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.chat-new {
|
||||||
|
.input-wrap {
|
||||||
|
display: flex;
|
||||||
|
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-search {
|
||||||
|
font-size: 1.5em;
|
||||||
|
float: right;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
padding-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-user-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--selectedPost, $fallback--lightBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
46
src/components/chat_new/chat_new.vue
Normal file
46
src/components/chat_new/chat_new.vue
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="nav"
|
||||||
|
class="panel-default panel chat-new"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="header"
|
||||||
|
class="panel-heading"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="go-back-button"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<i class="button-icon icon-left-open" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<div class="input-search">
|
||||||
|
<i class="button-icon icon-search" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model="query"
|
||||||
|
placeholder="Search people"
|
||||||
|
@input="onInput"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="member-list">
|
||||||
|
<div
|
||||||
|
v-for="user in availableUsers"
|
||||||
|
:key="user.id"
|
||||||
|
class="member"
|
||||||
|
>
|
||||||
|
<div @click.capture.prevent="goToChat(user)">
|
||||||
|
<BasicUserCard :user="user" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat_new.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import './chat_new.scss';
|
||||||
|
</style>
|
|
@ -84,54 +84,56 @@
|
||||||
max-width: 25em;
|
max-width: 25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-heading {
|
|
||||||
cursor: pointer;
|
|
||||||
.icon-comment-empty {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--text, $fallback--text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-window {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
max-height: 20em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-window-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message {
|
|
||||||
display: flex;
|
|
||||||
padding: 0.2em 0.5em
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-avatar {
|
|
||||||
img {
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
margin-right: 0.5em;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input {
|
|
||||||
display: flex;
|
|
||||||
textarea {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0.6em;
|
|
||||||
min-height: 3.5em;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel {
|
.chat-panel {
|
||||||
.title {
|
.chat-heading {
|
||||||
|
cursor: pointer;
|
||||||
|
.icon-comment-empty {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-window {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-window-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
padding: 0.2em 0.5em
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar {
|
||||||
|
img {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: $fallback--avatarRadius;
|
||||||
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
display: flex;
|
||||||
|
textarea {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0.6em;
|
||||||
|
min-height: 3.5em;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-panel {
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
26
src/components/chat_title/chat_title.js
Normal file
26
src/components/chat_title/chat_title.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
|
export default Vue.component('chat-title', {
|
||||||
|
name: 'ChatTitle',
|
||||||
|
components: {
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'user', 'withAvatar'
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
title () {
|
||||||
|
return this.user ? this.user.screen_name : ''
|
||||||
|
},
|
||||||
|
htmlTitle () {
|
||||||
|
return this.user ? this.user.name_html : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getUserProfileLink (user) {
|
||||||
|
return generateProfileLink(user.id, user.screen_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
67
src/components/chat_title/chat_title.vue
Normal file
67
src/components/chat_title/chat_title.vue
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<div
|
||||||
|
class="chat-title"
|
||||||
|
:title="title"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="withAvatar && user"
|
||||||
|
:to="getUserProfileLink(user)"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:user="user"
|
||||||
|
width="23px"
|
||||||
|
height="23px"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<span
|
||||||
|
class="username"
|
||||||
|
v-html="htmlTitle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat_title.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.chat-title {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
object-fit: contain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.still-image.avatar {
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
border-radius: $fallback--avatarAltRadius;
|
||||||
|
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||||
|
|
||||||
|
&.animated::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -52,7 +52,7 @@ export default {
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
display: block;
|
display: block;
|
||||||
content: '✔';
|
content: '✓';
|
||||||
transition: color 200ms;
|
transition: color 200ms;
|
||||||
width: 1.1em;
|
width: 1.1em;
|
||||||
height: 1.1em;
|
height: 1.1em;
|
||||||
|
|
|
@ -79,6 +79,20 @@ const EmojiInput = {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
placement: {
|
||||||
|
/**
|
||||||
|
* Forces the panel to take a specific position relative to the input element.
|
||||||
|
* The 'auto' placement chooses either bottom or top depending on which has the available space (when both have available space, bottom is preferred).
|
||||||
|
*/
|
||||||
|
required: false,
|
||||||
|
type: String, // 'auto', 'top', 'bottom'
|
||||||
|
default: 'auto'
|
||||||
|
},
|
||||||
|
newlineOnCtrlEnter: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -162,6 +176,11 @@ const EmojiInput = {
|
||||||
input.elm.removeEventListener('input', this.onInput)
|
input.elm.removeEventListener('input', this.onInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
showSuggestions: function (newValue) {
|
||||||
|
this.$emit('shown', newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
|
@ -190,7 +209,7 @@ const EmojiInput = {
|
||||||
this.$emit('input', newValue)
|
this.$emit('input', newValue)
|
||||||
this.caret = 0
|
this.caret = 0
|
||||||
},
|
},
|
||||||
insert ({ insertion, keepOpen }) {
|
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
||||||
const before = this.value.substring(0, this.caret) || ''
|
const before = this.value.substring(0, this.caret) || ''
|
||||||
const after = this.value.substring(this.caret) || ''
|
const after = this.value.substring(this.caret) || ''
|
||||||
|
|
||||||
|
@ -209,8 +228,8 @@ const EmojiInput = {
|
||||||
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
||||||
*/
|
*/
|
||||||
const isSpaceRegex = /\s/
|
const isSpaceRegex = /\s/
|
||||||
const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0 ? ' ' : ''
|
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
|
||||||
const spaceAfter = !isSpaceRegex.exec(after[0]) && this.padEmoji ? ' ' : ''
|
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
|
||||||
|
|
||||||
const newValue = [
|
const newValue = [
|
||||||
before,
|
before,
|
||||||
|
@ -367,6 +386,18 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
onKeyDown (e) {
|
onKeyDown (e) {
|
||||||
const { ctrlKey, shiftKey, key } = e
|
const { ctrlKey, shiftKey, key } = e
|
||||||
|
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
||||||
|
this.insert({ insertion: '\n', surroundingSpace: false })
|
||||||
|
// Ensure only one new line is added on macos
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Scroll the input element to the position of the cursor
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.input.elm.blur()
|
||||||
|
this.input.elm.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
// Disable suggestions hotkeys if suggestions are hidden
|
// Disable suggestions hotkeys if suggestions are hidden
|
||||||
if (!this.temporarilyHideSuggestions) {
|
if (!this.temporarilyHideSuggestions) {
|
||||||
if (key === 'Tab') {
|
if (key === 'Tab') {
|
||||||
|
@ -425,14 +456,29 @@ const EmojiInput = {
|
||||||
this.caret = selectionStart
|
this.caret = selectionStart
|
||||||
},
|
},
|
||||||
resize () {
|
resize () {
|
||||||
const { panel, picker } = this.$refs
|
const panel = this.$refs.panel
|
||||||
if (!panel) return
|
if (!panel) return
|
||||||
|
const picker = this.$refs.picker.$el
|
||||||
|
const panelBody = this.$refs['panel-body']
|
||||||
const { offsetHeight, offsetTop } = this.input.elm
|
const { offsetHeight, offsetTop } = this.input.elm
|
||||||
const offsetBottom = offsetTop + offsetHeight
|
const offsetBottom = offsetTop + offsetHeight
|
||||||
|
|
||||||
panel.style.top = offsetBottom + 'px'
|
this.setPlacement(panelBody, panel, offsetBottom)
|
||||||
picker.$el.style.top = offsetBottom + 'px'
|
this.setPlacement(picker, picker, offsetBottom)
|
||||||
picker.$el.style.bottom = 'auto'
|
},
|
||||||
|
setPlacement (container, target, offsetBottom) {
|
||||||
|
if (!container || !target) return
|
||||||
|
|
||||||
|
target.style.top = offsetBottom + 'px'
|
||||||
|
target.style.bottom = 'auto'
|
||||||
|
|
||||||
|
if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
|
||||||
|
target.style.top = 'auto'
|
||||||
|
target.style.bottom = this.input.elm.offsetHeight + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overflowsBottom (el) {
|
||||||
|
return el.getBoundingClientRect().bottom > window.innerHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,10 @@
|
||||||
class="autocomplete-panel"
|
class="autocomplete-panel"
|
||||||
:class="{ hide: !showSuggestions }"
|
:class="{ hide: !showSuggestions }"
|
||||||
>
|
>
|
||||||
<div class="autocomplete-panel-body">
|
<div
|
||||||
|
ref="panel-body"
|
||||||
|
class="autocomplete-panel-body"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(suggestion, index) in suggestions"
|
v-for="(suggestion, index) in suggestions"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|
|
@ -34,6 +34,16 @@ const ExtraButtons = {
|
||||||
navigator.clipboard.writeText(this.statusLink)
|
navigator.clipboard.writeText(this.statusLink)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
bookmarkStatus () {
|
||||||
|
this.$store.dispatch('bookmark', { id: this.status.id })
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
unbookmarkStatus () {
|
||||||
|
this.$store.dispatch('unbookmark', { id: this.status.id })
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -40,6 +40,22 @@
|
||||||
>
|
>
|
||||||
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!status.bookmarked"
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="bookmarkStatus"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="status.bookmarked"
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="unbookmarkStatus"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const FeaturesPanel = {
|
const FeaturesPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
chat: function () { return this.$store.state.instance.chatAvailable },
|
chat: function () { return this.$store.state.instance.chatAvailable },
|
||||||
|
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
|
||||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
||||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
<li v-if="chat">
|
<li v-if="chat">
|
||||||
{{ $t('features_panel.chat') }}
|
{{ $t('features_panel.chat') }}
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="pleromaChatMessages">
|
||||||
|
{{ $t('features_panel.pleroma_chat_messages') }}
|
||||||
|
</li>
|
||||||
<li v-if="gopher">
|
<li v-if="gopher">
|
||||||
{{ $t('features_panel.gopher') }}
|
{{ $t('features_panel.gopher') }}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -50,9 +50,7 @@
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: specificity problem with this and .attachments.attachment
|
.gallery-row-inner .attachment {
|
||||||
// we shouldn't have the need for .image here
|
|
||||||
.attachment.image {
|
|
||||||
margin: 0 0.5em 0 0;
|
margin: 0 0.5em 0 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
15
src/components/global_notice_list/global_notice_list.js
Normal file
15
src/components/global_notice_list/global_notice_list.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
const GlobalNoticeList = {
|
||||||
|
computed: {
|
||||||
|
notices () {
|
||||||
|
return this.$store.state.interface.globalNotices
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeNotice (notice) {
|
||||||
|
this.$store.dispatch('removeGlobalNotice', notice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalNoticeList
|
77
src/components/global_notice_list/global_notice_list.vue
Normal file
77
src/components/global_notice_list/global_notice_list.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div class="global-notice-list">
|
||||||
|
<div
|
||||||
|
v-for="(notice, index) in notices"
|
||||||
|
:key="index"
|
||||||
|
class="alert global-notice"
|
||||||
|
:class="{ ['global-' + notice.level]: true }"
|
||||||
|
>
|
||||||
|
<div class="notice-message">
|
||||||
|
{{ $t(notice.messageKey, notice.messageArgs) }}
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="closeNotice(notice)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./global_notice_list.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.global-notice-list {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.global-notice {
|
||||||
|
pointer-events: auto;
|
||||||
|
text-align: center;
|
||||||
|
width: 40em;
|
||||||
|
max-width: calc(100% - 3em);
|
||||||
|
display: flex;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
line-height: 2em;
|
||||||
|
.notice-message {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
flex: 0 0;
|
||||||
|
width: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-error {
|
||||||
|
background-color: var(--alertPopupError, $fallback--cRed);
|
||||||
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-warning {
|
||||||
|
background-color: var(--alertPopupWarning, $fallback--cOrange);
|
||||||
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-info {
|
||||||
|
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||||
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,6 +8,8 @@
|
||||||
v-if="type === 'image'"
|
v-if="type === 'image'"
|
||||||
class="modal-image"
|
class="modal-image"
|
||||||
:src="currentMedia.url"
|
:src="currentMedia.url"
|
||||||
|
:alt="currentMedia.description"
|
||||||
|
:title="currentMedia.description"
|
||||||
@touchstart.stop="mediaTouchStart"
|
@touchstart.stop="mediaTouchStart"
|
||||||
@touchmove.stop="mediaTouchMove"
|
@touchmove.stop="mediaTouchMove"
|
||||||
@click="hide"
|
@click="hide"
|
||||||
|
@ -18,6 +20,14 @@
|
||||||
:attachment="currentMedia"
|
:attachment="currentMedia"
|
||||||
:controls="true"
|
:controls="true"
|
||||||
/>
|
/>
|
||||||
|
<audio
|
||||||
|
v-if="type === 'audio'"
|
||||||
|
class="modal-image"
|
||||||
|
:src="currentMedia.url"
|
||||||
|
:alt="currentMedia.description"
|
||||||
|
:title="currentMedia.description"
|
||||||
|
controls
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="canNavigate"
|
v-if="canNavigate"
|
||||||
:title="$t('media_modal.previous')"
|
:title="$t('media_modal.previous')"
|
||||||
|
|
|
@ -61,7 +61,8 @@ const mediaUpload = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'dropFiles'
|
'dropFiles',
|
||||||
|
'disabled'
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
'dropFiles': function (fileInfos) {
|
'dropFiles': function (fileInfos) {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-upload">
|
<div
|
||||||
|
class="media-upload"
|
||||||
|
:class="{ disabled: disabled }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label"
|
class="label"
|
||||||
:title="$t('tool_tip.media_upload')"
|
:title="$t('tool_tip.media_upload')"
|
||||||
|
@ -14,6 +17,7 @@
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="uploadReady"
|
v-if="uploadReady"
|
||||||
|
:disabled="disabled"
|
||||||
type="file"
|
type="file"
|
||||||
style="position: fixed; top: -100em"
|
style="position: fixed; top: -100em"
|
||||||
multiple="true"
|
multiple="true"
|
||||||
|
@ -26,6 +30,8 @@
|
||||||
<script src="./media_upload.js" ></script>
|
<script src="./media_upload.js" ></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.media-upload {
|
.media-upload {
|
||||||
.label {
|
.label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||||
import Notifications from '../notifications/notifications.vue'
|
import Notifications from '../notifications/notifications.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
const MobileNav = {
|
const MobileNav = {
|
||||||
components: {
|
components: {
|
||||||
|
@ -30,7 +31,11 @@ const MobileNav = {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||||
sitename () { return this.$store.state.instance.name }
|
sitename () { return this.$store.state.instance.name },
|
||||||
|
isChat () {
|
||||||
|
return this.$route.name === 'chat'
|
||||||
|
},
|
||||||
|
...mapGetters(['unreadChatCount'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleMobileSidebar () {
|
toggleMobileSidebar () {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<nav
|
<nav
|
||||||
id="nav"
|
id="nav"
|
||||||
class="nav-bar container"
|
class="nav-bar container"
|
||||||
|
:class="{ 'mobile-hidden': isChat }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mobile-inner-nav"
|
class="mobile-inner-nav"
|
||||||
|
@ -15,6 +16,10 @@
|
||||||
@click.stop.prevent="toggleMobileSidebar()"
|
@click.stop.prevent="toggleMobileSidebar()"
|
||||||
>
|
>
|
||||||
<i class="button-icon icon-menu" />
|
<i class="button-icon icon-menu" />
|
||||||
|
<div
|
||||||
|
v-if="unreadChatCount"
|
||||||
|
class="alert-dot"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="!hideSitename"
|
v-if="!hideSitename"
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
|
|
||||||
|
const HIDDEN_FOR_PAGES = new Set([
|
||||||
|
'chats',
|
||||||
|
'chat'
|
||||||
|
])
|
||||||
|
|
||||||
const MobilePostStatusButton = {
|
const MobilePostStatusButton = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -27,6 +32,8 @@ const MobilePostStatusButton = {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
isHidden () {
|
isHidden () {
|
||||||
|
if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true }
|
||||||
|
|
||||||
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||||
},
|
},
|
||||||
autohideFloatingPostButton () {
|
autohideFloatingPostButton () {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
created () {
|
created () {
|
||||||
|
@ -6,13 +6,17 @@ const NavPanel = {
|
||||||
this.$store.dispatch('startFetchingFollowRequests')
|
this.$store.dispatch('startFetchingFollowRequests')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: {
|
||||||
currentUser: state => state.users.currentUser,
|
...mapState({
|
||||||
chat: state => state.chat.channel,
|
currentUser: state => state.users.currentUser,
|
||||||
followRequestCount: state => state.api.followRequests.length,
|
chat: state => state.chat.channel,
|
||||||
privateMode: state => state.instance.private,
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
federating: state => state.instance.federating
|
privateMode: state => state.instance.private,
|
||||||
})
|
federating: state => state.instance.federating,
|
||||||
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||||
|
}),
|
||||||
|
...mapGetters(['unreadChatCount'])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NavPanel
|
export default NavPanel
|
||||||
|
|
|
@ -17,6 +17,22 @@
|
||||||
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
|
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="currentUser">
|
||||||
|
<router-link :to="{ name: 'bookmarks'}">
|
||||||
|
<i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<li v-if="currentUser && pleromaChatMessagesAvailable">
|
||||||
|
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
|
||||||
|
<div
|
||||||
|
v-if="unreadChatCount"
|
||||||
|
class="badge badge-notification unread-chat-count"
|
||||||
|
>
|
||||||
|
{{ unreadChatCount }}
|
||||||
|
</div>
|
||||||
|
<i class="button-icon icon-chat" /> {{ $t("nav.chats") }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li v-if="currentUser && currentUser.locked">
|
<li v-if="currentUser && currentUser.locked">
|
||||||
<router-link :to="{ name: 'friend-requests' }">
|
<router-link :to="{ name: 'friend-requests' }">
|
||||||
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
|
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
|
@ -81,7 +82,10 @@ const Notification = {
|
||||||
},
|
},
|
||||||
isStatusNotification () {
|
isStatusNotification () {
|
||||||
return isStatusNotification(this.notification.type)
|
return isStatusNotification(this.notification.type)
|
||||||
}
|
},
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
import Notification from '../notification/notification.vue'
|
import Notification from '../notification/notification.vue'
|
||||||
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
|
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
|
||||||
import {
|
import {
|
||||||
|
@ -27,6 +28,11 @@ const Notifications = {
|
||||||
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
const store = this.$store
|
||||||
|
const credentials = store.state.users.currentUser.credentials
|
||||||
|
notificationsFetcher.fetchAndUpdate({ store, credentials })
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mainClass () {
|
mainClass () {
|
||||||
return this.minimalMode ? '' : 'panel panel-default'
|
return this.minimalMode ? '' : 'panel panel-default'
|
||||||
|
@ -46,23 +52,22 @@ const Notifications = {
|
||||||
unseenCount () {
|
unseenCount () {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
|
unseenCountTitle () {
|
||||||
|
return this.unseenCount + (this.unreadChatCount)
|
||||||
|
},
|
||||||
loading () {
|
loading () {
|
||||||
return this.$store.state.statuses.notifications.loading
|
return this.$store.state.statuses.notifications.loading
|
||||||
},
|
},
|
||||||
notificationsToDisplay () {
|
notificationsToDisplay () {
|
||||||
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
||||||
}
|
},
|
||||||
|
...mapGetters(['unreadChatCount'])
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Notification
|
Notification
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
const { dispatch } = this.$store
|
|
||||||
|
|
||||||
dispatch('fetchAndUpdateNotifications')
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
unseenCount (count) {
|
unseenCountTitle (count) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
this.$store.dispatch('setPageTitle', `(${count})`)
|
this.$store.dispatch('setPageTitle', `(${count})`)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -118,6 +118,11 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-left: 0.8em;
|
padding-left: 0.8em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
.timeago {
|
||||||
|
min-width: 3em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-reaction-emoji {
|
.emoji-reaction-emoji {
|
||||||
|
|
|
@ -3,11 +3,13 @@ import MediaUpload from '../media_upload/media_upload.vue'
|
||||||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||||
import EmojiInput from '../emoji_input/emoji_input.vue'
|
import EmojiInput from '../emoji_input/emoji_input.vue'
|
||||||
import PollForm from '../poll/poll_form.vue'
|
import PollForm from '../poll/poll_form.vue'
|
||||||
|
import Attachment from '../attachment/attachment.vue'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
import { reject, map, uniqBy } from 'lodash'
|
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
import suggestor from '../emoji_input/suggestor.js'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
|
||||||
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
|
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
|
||||||
|
@ -31,21 +33,42 @@ const PostStatusForm = {
|
||||||
'repliedUser',
|
'repliedUser',
|
||||||
'attentions',
|
'attentions',
|
||||||
'copyMessageScope',
|
'copyMessageScope',
|
||||||
'subject'
|
'subject',
|
||||||
|
'disableSubject',
|
||||||
|
'disableScopeSelector',
|
||||||
|
'disableNotice',
|
||||||
|
'disableLockWarning',
|
||||||
|
'disablePolls',
|
||||||
|
'disableSensitivityCheckbox',
|
||||||
|
'disableSubmit',
|
||||||
|
'disablePreview',
|
||||||
|
'placeholder',
|
||||||
|
'maxHeight',
|
||||||
|
'postHandler',
|
||||||
|
'preserveFocus',
|
||||||
|
'autoFocus',
|
||||||
|
'fileLimit',
|
||||||
|
'submitOnEnter',
|
||||||
|
'emojiPickerPlacement'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload,
|
MediaUpload,
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
PollForm,
|
PollForm,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
Attachment,
|
||||||
|
StatusContent
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resize(this.$refs.textarea)
|
this.resize(this.$refs.textarea)
|
||||||
const textLength = this.$refs.textarea.value.length
|
|
||||||
this.$refs.textarea.setSelectionRange(textLength, textLength)
|
|
||||||
|
|
||||||
if (this.replyTo) {
|
if (this.replyTo) {
|
||||||
|
const textLength = this.$refs.textarea.value.length
|
||||||
|
this.$refs.textarea.setSelectionRange(textLength, textLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.replyTo || this.autoFocus) {
|
||||||
this.$refs.textarea.focus()
|
this.$refs.textarea.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -68,7 +91,7 @@ const PostStatusForm = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dropFiles: [],
|
dropFiles: [],
|
||||||
submitDisabled: false,
|
uploadingFiles: false,
|
||||||
error: null,
|
error: null,
|
||||||
posting: false,
|
posting: false,
|
||||||
highlighted: 0,
|
highlighted: 0,
|
||||||
|
@ -78,13 +101,17 @@ const PostStatusForm = {
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
files: [],
|
files: [],
|
||||||
poll: {},
|
poll: {},
|
||||||
|
mediaDescriptions: {},
|
||||||
visibility: scope,
|
visibility: scope,
|
||||||
contentType
|
contentType
|
||||||
},
|
},
|
||||||
caret: 0,
|
caret: 0,
|
||||||
pollFormVisible: false,
|
pollFormVisible: false,
|
||||||
showDropIcon: 'hide',
|
showDropIcon: 'hide',
|
||||||
dropStopTimeout: null
|
dropStopTimeout: null,
|
||||||
|
preview: null,
|
||||||
|
previewLoading: false,
|
||||||
|
emojiInputShown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -153,28 +180,52 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
pollsAvailable () {
|
pollsAvailable () {
|
||||||
return this.$store.state.instance.pollsAvailable &&
|
return this.$store.state.instance.pollsAvailable &&
|
||||||
this.$store.state.instance.pollLimits.max_options >= 2
|
this.$store.state.instance.pollLimits.max_options >= 2 &&
|
||||||
|
this.disablePolls !== true
|
||||||
},
|
},
|
||||||
hideScopeNotice () {
|
hideScopeNotice () {
|
||||||
return this.$store.getters.mergedConfig.hideScopeNotice
|
return this.disableNotice || this.$store.getters.mergedConfig.hideScopeNotice
|
||||||
},
|
},
|
||||||
pollContentError () {
|
pollContentError () {
|
||||||
return this.pollFormVisible &&
|
return this.pollFormVisible &&
|
||||||
this.newStatus.poll &&
|
this.newStatus.poll &&
|
||||||
this.newStatus.poll.error
|
this.newStatus.poll.error
|
||||||
},
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
showPreview () {
|
||||||
|
return !this.disablePreview && (!!this.preview || this.previewLoading)
|
||||||
|
},
|
||||||
|
emptyStatus () {
|
||||||
|
return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
|
||||||
|
},
|
||||||
|
uploadFileLimitReached () {
|
||||||
|
return this.newStatus.files.length >= this.fileLimit
|
||||||
|
},
|
||||||
|
...mapGetters(['mergedConfig']),
|
||||||
|
...mapState({
|
||||||
|
mobileLayout: state => state.interface.mobileLayout
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'newStatus.contentType': function () {
|
||||||
|
this.autoPreview()
|
||||||
|
},
|
||||||
|
'newStatus.spoilerText': function () {
|
||||||
|
this.autoPreview()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
postStatus (newStatus) {
|
async postStatus (event, newStatus, opts = {}) {
|
||||||
if (this.posting) { return }
|
if (this.posting) { return }
|
||||||
if (this.submitDisabled) { return }
|
if (this.disableSubmit) { return }
|
||||||
|
if (this.emojiInputShown) { return }
|
||||||
|
if (this.submitOnEnter) {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
if (this.newStatus.status === '') {
|
if (this.emptyStatus) {
|
||||||
if (this.newStatus.files.length === 0) {
|
this.error = this.$t('post_status.empty_status_error')
|
||||||
this.error = 'Cannot post an empty status with no files'
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
||||||
|
@ -184,7 +235,16 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.posting = true
|
this.posting = true
|
||||||
statusPoster.postStatus({
|
|
||||||
|
try {
|
||||||
|
await this.setAllMediaDescriptions()
|
||||||
|
} catch (e) {
|
||||||
|
this.error = this.$t('post_status.media_description_error')
|
||||||
|
this.posting = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const postingOptions = {
|
||||||
status: newStatus.status,
|
status: newStatus.status,
|
||||||
spoilerText: newStatus.spoilerText || null,
|
spoilerText: newStatus.spoilerText || null,
|
||||||
visibility: newStatus.visibility,
|
visibility: newStatus.visibility,
|
||||||
|
@ -194,7 +254,11 @@ const PostStatusForm = {
|
||||||
inReplyToStatusId: this.replyTo,
|
inReplyToStatusId: this.replyTo,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
poll
|
poll
|
||||||
}).then((data) => {
|
}
|
||||||
|
|
||||||
|
const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus
|
||||||
|
|
||||||
|
postHandler(postingOptions).then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
this.newStatus = {
|
this.newStatus = {
|
||||||
status: '',
|
status: '',
|
||||||
|
@ -202,43 +266,105 @@ const PostStatusForm = {
|
||||||
files: [],
|
files: [],
|
||||||
visibility: newStatus.visibility,
|
visibility: newStatus.visibility,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
poll: {}
|
poll: {},
|
||||||
|
mediaDescriptions: {}
|
||||||
}
|
}
|
||||||
this.pollFormVisible = false
|
this.pollFormVisible = false
|
||||||
this.$refs.mediaUpload.clearFile()
|
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||||
this.clearPollForm()
|
this.clearPollForm()
|
||||||
this.$emit('posted')
|
this.$emit('posted', data)
|
||||||
|
if (this.preserveFocus) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.textarea.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
let el = this.$el.querySelector('textarea')
|
let el = this.$el.querySelector('textarea')
|
||||||
el.style.height = 'auto'
|
el.style.height = 'auto'
|
||||||
el.style.height = undefined
|
el.style.height = undefined
|
||||||
this.error = null
|
this.error = null
|
||||||
|
if (this.preview) this.previewStatus()
|
||||||
} else {
|
} else {
|
||||||
this.error = data.error
|
this.error = data.error
|
||||||
}
|
}
|
||||||
this.posting = false
|
this.posting = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
previewStatus () {
|
||||||
|
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
||||||
|
this.preview = { error: this.$t('post_status.preview_empty') }
|
||||||
|
this.previewLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newStatus = this.newStatus
|
||||||
|
this.previewLoading = true
|
||||||
|
statusPoster.postStatus({
|
||||||
|
status: newStatus.status,
|
||||||
|
spoilerText: newStatus.spoilerText || null,
|
||||||
|
visibility: newStatus.visibility,
|
||||||
|
sensitive: newStatus.nsfw,
|
||||||
|
media: [],
|
||||||
|
store: this.$store,
|
||||||
|
inReplyToStatusId: this.replyTo,
|
||||||
|
contentType: newStatus.contentType,
|
||||||
|
poll: {},
|
||||||
|
preview: true
|
||||||
|
}).then((data) => {
|
||||||
|
// Don't apply preview if not loading, because it means
|
||||||
|
// user has closed the preview manually.
|
||||||
|
if (!this.previewLoading) return
|
||||||
|
if (!data.error) {
|
||||||
|
this.preview = data
|
||||||
|
} else {
|
||||||
|
this.preview = { error: data.error }
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
this.preview = { error }
|
||||||
|
}).finally(() => {
|
||||||
|
this.previewLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500),
|
||||||
|
autoPreview () {
|
||||||
|
if (!this.preview) return
|
||||||
|
this.previewLoading = true
|
||||||
|
this.debouncePreviewStatus()
|
||||||
|
},
|
||||||
|
closePreview () {
|
||||||
|
this.preview = null
|
||||||
|
this.previewLoading = false
|
||||||
|
},
|
||||||
|
togglePreview () {
|
||||||
|
if (this.showPreview) {
|
||||||
|
this.closePreview()
|
||||||
|
} else {
|
||||||
|
this.previewStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
addMediaFile (fileInfo) {
|
addMediaFile (fileInfo) {
|
||||||
this.newStatus.files.push(fileInfo)
|
this.newStatus.files.push(fileInfo)
|
||||||
|
this.$emit('resize', { delayed: true })
|
||||||
},
|
},
|
||||||
removeMediaFile (fileInfo) {
|
removeMediaFile (fileInfo) {
|
||||||
let index = this.newStatus.files.indexOf(fileInfo)
|
let index = this.newStatus.files.indexOf(fileInfo)
|
||||||
this.newStatus.files.splice(index, 1)
|
this.newStatus.files.splice(index, 1)
|
||||||
|
this.$emit('resize')
|
||||||
},
|
},
|
||||||
uploadFailed (errString, templateArgs) {
|
uploadFailed (errString, templateArgs) {
|
||||||
templateArgs = templateArgs || {}
|
templateArgs = templateArgs || {}
|
||||||
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
|
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
|
||||||
},
|
},
|
||||||
disableSubmit () {
|
startedUploadingFiles () {
|
||||||
this.submitDisabled = true
|
this.uploadingFiles = true
|
||||||
},
|
},
|
||||||
enableSubmit () {
|
finishedUploadingFiles () {
|
||||||
this.submitDisabled = false
|
this.$emit('resize')
|
||||||
|
this.uploadingFiles = false
|
||||||
},
|
},
|
||||||
type (fileInfo) {
|
type (fileInfo) {
|
||||||
return fileTypeService.fileType(fileInfo.mimetype)
|
return fileTypeService.fileType(fileInfo.mimetype)
|
||||||
},
|
},
|
||||||
paste (e) {
|
paste (e) {
|
||||||
|
this.autoPreview()
|
||||||
this.resize(e)
|
this.resize(e)
|
||||||
if (e.clipboardData.files.length > 0) {
|
if (e.clipboardData.files.length > 0) {
|
||||||
// prevent pasting of file as text
|
// prevent pasting of file as text
|
||||||
|
@ -266,13 +392,14 @@ const PostStatusForm = {
|
||||||
this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
|
this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
|
||||||
},
|
},
|
||||||
fileDrag (e) {
|
fileDrag (e) {
|
||||||
e.dataTransfer.dropEffect = 'copy'
|
e.dataTransfer.dropEffect = this.uploadFileLimitReached ? 'none' : 'copy'
|
||||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||||
clearTimeout(this.dropStopTimeout)
|
clearTimeout(this.dropStopTimeout)
|
||||||
this.showDropIcon = 'show'
|
this.showDropIcon = 'show'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEmojiInputInput (e) {
|
onEmojiInputInput (e) {
|
||||||
|
this.autoPreview()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.resize(this.$refs['textarea'])
|
this.resize(this.$refs['textarea'])
|
||||||
})
|
})
|
||||||
|
@ -284,6 +411,7 @@ const PostStatusForm = {
|
||||||
// Reset to default height for empty form, nothing else to do here.
|
// Reset to default height for empty form, nothing else to do here.
|
||||||
if (target.value === '') {
|
if (target.value === '') {
|
||||||
target.style.height = null
|
target.style.height = null
|
||||||
|
this.$emit('resize')
|
||||||
this.$refs['emoji-input'].resize()
|
this.$refs['emoji-input'].resize()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -336,8 +464,10 @@ const PostStatusForm = {
|
||||||
|
|
||||||
// BEGIN content size update
|
// BEGIN content size update
|
||||||
target.style.height = 'auto'
|
target.style.height = 'auto'
|
||||||
const newHeight = target.scrollHeight - vertPadding
|
const heightWithoutPadding = target.scrollHeight - vertPadding
|
||||||
|
const newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding
|
||||||
target.style.height = `${newHeight}px`
|
target.style.height = `${newHeight}px`
|
||||||
|
this.$emit('resize', newHeight)
|
||||||
// END content size update
|
// END content size update
|
||||||
|
|
||||||
// We check where the bottom border of form-bottom element is, this uses findOffset
|
// We check where the bottom border of form-bottom element is, this uses findOffset
|
||||||
|
@ -388,6 +518,18 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
dismissScopeNotice () {
|
dismissScopeNotice () {
|
||||||
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
|
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
|
||||||
|
},
|
||||||
|
setMediaDescription (id) {
|
||||||
|
const description = this.newStatus.mediaDescriptions[id]
|
||||||
|
if (!description || description.trim() === '') return
|
||||||
|
return statusPoster.setMediaDescription({ store: this.$store, id, description })
|
||||||
|
},
|
||||||
|
setAllMediaDescriptions () {
|
||||||
|
const ids = this.newStatus.files.map(file => file.id)
|
||||||
|
return Promise.all(ids.map(id => this.setMediaDescription(id)))
|
||||||
|
},
|
||||||
|
handleEmojiInputShow (value) {
|
||||||
|
this.emojiInputShown = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,20 @@
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@submit.prevent="postStatus(newStatus)"
|
@submit.prevent
|
||||||
@dragover.prevent="fileDrag"
|
@dragover.prevent="fileDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-show="showDropIcon !== 'hide'"
|
v-show="showDropIcon !== 'hide'"
|
||||||
:style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
|
:style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
|
||||||
class="drop-indicator icon-upload"
|
class="drop-indicator"
|
||||||
|
:class="[uploadFileLimitReached ? 'icon-block' : 'icon-upload']"
|
||||||
@dragleave="fileDragStop"
|
@dragleave="fileDragStop"
|
||||||
@drop.stop="fileDrop"
|
@drop.stop="fileDrop"
|
||||||
/>
|
/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<i18n
|
<i18n
|
||||||
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
|
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning"
|
||||||
path="post_status.account_not_locked_warning"
|
path="post_status.account_not_locked_warning"
|
||||||
tag="p"
|
tag="p"
|
||||||
class="visibility-notice"
|
class="visibility-notice"
|
||||||
|
@ -69,15 +70,55 @@
|
||||||
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
||||||
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="!disablePreview"
|
||||||
|
class="preview-heading faint"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="preview-toggle faint"
|
||||||
|
@click.stop.prevent="togglePreview"
|
||||||
|
>
|
||||||
|
{{ $t('post_status.preview') }}
|
||||||
|
<i
|
||||||
|
class="icon-down-open"
|
||||||
|
:style="{ transform: showPreview ? 'rotate(0deg)' : 'rotate(-90deg)' }"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<i
|
||||||
|
v-show="previewLoading"
|
||||||
|
class="icon-spin3 animate-spin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showPreview"
|
||||||
|
class="preview-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!preview"
|
||||||
|
class="preview-status"
|
||||||
|
>
|
||||||
|
{{ $t('general.loading') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="preview.error"
|
||||||
|
class="preview-status preview-error"
|
||||||
|
>
|
||||||
|
{{ preview.error }}
|
||||||
|
</div>
|
||||||
|
<StatusContent
|
||||||
|
v-else
|
||||||
|
:status="preview"
|
||||||
|
class="preview-status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-if="newStatus.spoilerText || alwaysShowSubject"
|
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
enable-emoji-picker
|
enable-emoji-picker
|
||||||
:suggest="emojiSuggestor"
|
:suggest="emojiSuggestor"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
|
@ -89,23 +130,28 @@
|
||||||
ref="emoji-input"
|
ref="emoji-input"
|
||||||
v-model="newStatus.status"
|
v-model="newStatus.status"
|
||||||
:suggest="emojiUserSuggestor"
|
:suggest="emojiUserSuggestor"
|
||||||
|
:placement="emojiPickerPlacement"
|
||||||
class="form-control main-input"
|
class="form-control main-input"
|
||||||
enable-emoji-picker
|
enable-emoji-picker
|
||||||
hide-emoji-button
|
hide-emoji-button
|
||||||
|
:newline-on-ctrl-enter="submitOnEnter"
|
||||||
enable-sticker-picker
|
enable-sticker-picker
|
||||||
@input="onEmojiInputInput"
|
@input="onEmojiInputInput"
|
||||||
@sticker-uploaded="addMediaFile"
|
@sticker-uploaded="addMediaFile"
|
||||||
@sticker-upload-failed="uploadFailed"
|
@sticker-upload-failed="uploadFailed"
|
||||||
|
@shown="handleEmojiInputShow"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
v-model="newStatus.status"
|
v-model="newStatus.status"
|
||||||
:placeholder="$t('post_status.default')"
|
:placeholder="placeholder || $t('post_status.default')"
|
||||||
rows="1"
|
rows="1"
|
||||||
:disabled="posting"
|
:disabled="posting"
|
||||||
class="form-post-body"
|
class="form-post-body"
|
||||||
@keydown.meta.enter="postStatus(newStatus)"
|
:class="{ 'scrollable-form': !!maxHeight }"
|
||||||
@keydown.ctrl.enter="postStatus(newStatus)"
|
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||||
|
@keydown.meta.enter="postStatus($event, newStatus)"
|
||||||
|
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
||||||
@input="resize"
|
@input="resize"
|
||||||
@compositionupdate="resize"
|
@compositionupdate="resize"
|
||||||
@paste="paste"
|
@paste="paste"
|
||||||
|
@ -118,7 +164,10 @@
|
||||||
{{ charactersLeft }}
|
{{ charactersLeft }}
|
||||||
</p>
|
</p>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<div class="visibility-tray">
|
<div
|
||||||
|
v-if="!disableScopeSelector"
|
||||||
|
class="visibility-tray"
|
||||||
|
>
|
||||||
<scope-selector
|
<scope-selector
|
||||||
:show-all="showAllScopes"
|
:show-all="showAllScopes"
|
||||||
:user-default="userDefaultScope"
|
:user-default="userDefaultScope"
|
||||||
|
@ -176,10 +225,11 @@
|
||||||
ref="mediaUpload"
|
ref="mediaUpload"
|
||||||
class="media-upload-icon"
|
class="media-upload-icon"
|
||||||
:drop-files="dropFiles"
|
:drop-files="dropFiles"
|
||||||
@uploading="disableSubmit"
|
:disabled="uploadFileLimitReached"
|
||||||
|
@uploading="startedUploadingFiles"
|
||||||
@uploaded="addMediaFile"
|
@uploaded="addMediaFile"
|
||||||
@upload-failed="uploadFailed"
|
@upload-failed="uploadFailed"
|
||||||
@all-uploaded="enableSubmit"
|
@all-uploaded="finishedUploadingFiles"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="emoji-icon"
|
class="emoji-icon"
|
||||||
|
@ -216,11 +266,13 @@
|
||||||
>
|
>
|
||||||
{{ $t('general.submit') }}
|
{{ $t('general.submit') }}
|
||||||
</button>
|
</button>
|
||||||
|
<!-- touchstart is used to keep the OSK at the same position after a message send -->
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
:disabled="submitDisabled"
|
:disabled="uploadingFiles || disableSubmit"
|
||||||
type="submit"
|
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
|
@touchstart.stop.prevent="postStatus($event, newStatus)"
|
||||||
|
@click.stop.prevent="postStatus($event, newStatus)"
|
||||||
>
|
>
|
||||||
{{ $t('general.submit') }}
|
{{ $t('general.submit') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -245,31 +297,22 @@
|
||||||
class="fa button-icon icon-cancel"
|
class="fa button-icon icon-cancel"
|
||||||
@click="removeMediaFile(file)"
|
@click="removeMediaFile(file)"
|
||||||
/>
|
/>
|
||||||
<div class="media-upload-container attachment">
|
<attachment
|
||||||
<img
|
:attachment="file"
|
||||||
v-if="type(file) === 'image'"
|
:set-media="() => $store.dispatch('setMedia', newStatus.files)"
|
||||||
class="thumbnail media-upload"
|
size="small"
|
||||||
:src="file.url"
|
allow-play="false"
|
||||||
>
|
/>
|
||||||
<video
|
<input
|
||||||
v-if="type(file) === 'video'"
|
v-model="newStatus.mediaDescriptions[file.id]"
|
||||||
:src="file.url"
|
type="text"
|
||||||
controls
|
:placeholder="$t('post_status.media_description')"
|
||||||
/>
|
@keydown.enter.prevent=""
|
||||||
<audio
|
>
|
||||||
v-if="type(file) === 'audio'"
|
|
||||||
:src="file.url"
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="type(file) === 'unknown'"
|
|
||||||
:href="file.url"
|
|
||||||
>{{ file.url }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="newStatus.files.length > 0"
|
v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
|
||||||
class="upload_settings"
|
class="upload_settings"
|
||||||
>
|
>
|
||||||
<Checkbox v-model="newStatus.nsfw">
|
<Checkbox v-model="newStatus.nsfw">
|
||||||
|
@ -303,14 +346,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-status-form {
|
.post-status-form {
|
||||||
.visibility-tray {
|
position: relative;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-status-form {
|
|
||||||
.form-bottom {
|
.form-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -336,6 +373,48 @@
|
||||||
max-width: 10em;
|
max-width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-heading {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.icon-spin3 {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-toggle {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-down-open {
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-error {
|
||||||
|
font-style: italic;
|
||||||
|
color: $fallback--faint;
|
||||||
|
color: var(--faint, $fallback--faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status {
|
||||||
|
border: 1px solid $fallback--border;
|
||||||
|
border: 1px solid var(--border, $fallback--border);
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
.text-format {
|
.text-format {
|
||||||
.only-format {
|
.only-format {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
|
@ -343,6 +422,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-tray {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.media-upload-icon, .poll-icon, .emoji-icon {
|
.media-upload-icon, .poll-icon, .emoji-icon {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -354,6 +439,19 @@
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
i {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: $fallback--icon;
|
||||||
|
color: var(--btnDisabledText, $fallback--icon);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $fallback--icon;
|
||||||
|
color: var(--btnDisabledText, $fallback--icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order is not necessary but a good indicator
|
// Order is not necessary but a good indicator
|
||||||
|
@ -381,11 +479,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-upload-wrapper {
|
.media-upload-wrapper {
|
||||||
flex: 0 0 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
min-width: 50px;
|
|
||||||
margin-right: .2em;
|
margin-right: .2em;
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
|
width: 18em;
|
||||||
|
|
||||||
.icon-cancel {
|
.icon-cancel {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -399,6 +495,20 @@
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img, video {
|
||||||
|
object-fit: contain;
|
||||||
|
max-height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
max-height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-input-wrapper {
|
.status-input-wrapper {
|
||||||
|
@ -408,28 +518,13 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments {
|
.media-upload-wrapper .attachments {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
|
|
||||||
.attachment {
|
.attachment {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 auto;
|
|
||||||
border: 1px solid $fallback--border;
|
|
||||||
border: 1px solid var(--border, $fallback--border);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
audio {
|
|
||||||
min-width: 300px;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
text-align: left;
|
|
||||||
line-height: 1.2;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
@ -482,6 +577,10 @@
|
||||||
padding-bottom: 1.75em;
|
padding-bottom: 1.75em;
|
||||||
min-height: 1px;
|
min-height: 1px;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&.scrollable-form {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-input {
|
.main-input {
|
||||||
|
@ -544,4 +643,11 @@
|
||||||
border: 2px dashed var(--text, $fallback--text);
|
border: 2px dashed var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: unify with attachment.vue (otherwise the uploaded images are not minified unless a status with an attachment was displayed before)
|
||||||
|
img.media-upload, .media-upload-container > video {
|
||||||
|
line-height: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
>.panel-body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ const FilteringTab = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
|
},
|
||||||
|
replyVisibility () {
|
||||||
|
this.$store.dispatch('queueFlushAll')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import unescape from 'lodash/unescape'
|
import unescape from 'lodash/unescape'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||||
|
@ -16,6 +17,7 @@ const ProfileTab = {
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
newLocked: this.$store.state.users.currentUser.locked,
|
||||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||||
|
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
||||||
|
@ -63,6 +65,45 @@ const ProfileTab = {
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.state.instance.emoji,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
] })
|
] })
|
||||||
|
},
|
||||||
|
userSuggestor () {
|
||||||
|
return suggestor({
|
||||||
|
users: this.$store.state.users.users,
|
||||||
|
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fieldsLimits () {
|
||||||
|
return this.$store.state.instance.fieldsLimits
|
||||||
|
},
|
||||||
|
maxFields () {
|
||||||
|
return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
|
||||||
|
},
|
||||||
|
defaultAvatar () {
|
||||||
|
return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar
|
||||||
|
},
|
||||||
|
defaultBanner () {
|
||||||
|
return this.$store.state.instance.server + this.$store.state.instance.defaultBanner
|
||||||
|
},
|
||||||
|
isDefaultAvatar () {
|
||||||
|
const baseAvatar = this.$store.state.instance.defaultAvatar
|
||||||
|
return !(this.$store.state.users.currentUser.profile_image_url) ||
|
||||||
|
this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar)
|
||||||
|
},
|
||||||
|
isDefaultBanner () {
|
||||||
|
const baseBanner = this.$store.state.instance.defaultBanner
|
||||||
|
return !(this.$store.state.users.currentUser.cover_photo) ||
|
||||||
|
this.$store.state.users.currentUser.cover_photo.includes(baseBanner)
|
||||||
|
},
|
||||||
|
isDefaultBackground () {
|
||||||
|
return !(this.$store.state.users.currentUser.background_image)
|
||||||
|
},
|
||||||
|
avatarImgSrc () {
|
||||||
|
const src = this.$store.state.users.currentUser.profile_image_url_original
|
||||||
|
return (!src) ? this.defaultAvatar : src
|
||||||
|
},
|
||||||
|
bannerImgSrc () {
|
||||||
|
const src = this.$store.state.users.currentUser.cover_photo
|
||||||
|
return (!src) ? this.defaultBanner : src
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -75,6 +116,7 @@ const ProfileTab = {
|
||||||
// Backend notation.
|
// Backend notation.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
display_name: this.newName,
|
display_name: this.newName,
|
||||||
|
fields_attributes: this.newFields.filter(el => el != null),
|
||||||
default_scope: this.newDefaultScope,
|
default_scope: this.newDefaultScope,
|
||||||
no_rich_text: this.newNoRichText,
|
no_rich_text: this.newNoRichText,
|
||||||
hide_follows: this.hideFollows,
|
hide_follows: this.hideFollows,
|
||||||
|
@ -87,6 +129,8 @@ const ProfileTab = {
|
||||||
show_role: this.showRole
|
show_role: this.showRole
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
} }).then((user) => {
|
} }).then((user) => {
|
||||||
|
this.newFields.splice(user.fields.length)
|
||||||
|
merge(this.newFields, user.fields)
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
})
|
})
|
||||||
|
@ -94,6 +138,16 @@ const ProfileTab = {
|
||||||
changeVis (visibility) {
|
changeVis (visibility) {
|
||||||
this.newDefaultScope = visibility
|
this.newDefaultScope = visibility
|
||||||
},
|
},
|
||||||
|
addField () {
|
||||||
|
if (this.newFields.length < this.maxFields) {
|
||||||
|
this.newFields.push({ name: '', value: '' })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
deleteField (index, event) {
|
||||||
|
this.$delete(this.newFields, index)
|
||||||
|
},
|
||||||
uploadFile (slot, e) {
|
uploadFile (slot, e) {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (!file) { return }
|
if (!file) { return }
|
||||||
|
@ -123,11 +177,29 @@ const ProfileTab = {
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
},
|
},
|
||||||
|
resetAvatar () {
|
||||||
|
const confirmed = window.confirm(this.$t('settings.reset_avatar_confirm'))
|
||||||
|
if (confirmed) {
|
||||||
|
this.submitAvatar(undefined, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetBanner () {
|
||||||
|
const confirmed = window.confirm(this.$t('settings.reset_banner_confirm'))
|
||||||
|
if (confirmed) {
|
||||||
|
this.submitBanner('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetBackground () {
|
||||||
|
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
|
||||||
|
if (confirmed) {
|
||||||
|
this.submitBackground('')
|
||||||
|
}
|
||||||
|
},
|
||||||
submitAvatar (cropper, file) {
|
submitAvatar (cropper, file) {
|
||||||
const that = this
|
const that = this
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
function updateAvatar (avatar) {
|
function updateAvatar (avatar) {
|
||||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
that.$store.state.api.backendInteractor.updateProfileImages({ avatar })
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
that.$store.commit('addNewUsers', [user])
|
that.$store.commit('addNewUsers', [user])
|
||||||
that.$store.commit('setCurrentUser', user)
|
that.$store.commit('setCurrentUser', user)
|
||||||
|
@ -145,11 +217,11 @@ const ProfileTab = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submitBanner () {
|
submitBanner (banner) {
|
||||||
if (!this.bannerPreview) { return }
|
if (!this.bannerPreview && banner !== '') { return }
|
||||||
|
|
||||||
this.bannerUploading = true
|
this.bannerUploading = true
|
||||||
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
this.$store.state.api.backendInteractor.updateProfileImages({ banner })
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
|
@ -160,11 +232,11 @@ const ProfileTab = {
|
||||||
})
|
})
|
||||||
.then(() => { this.bannerUploading = false })
|
.then(() => { this.bannerUploading = false })
|
||||||
},
|
},
|
||||||
submitBg () {
|
submitBackground (background) {
|
||||||
if (!this.backgroundPreview) { return }
|
if (!this.backgroundPreview && background !== '') { return }
|
||||||
let background = this.background
|
|
||||||
this.backgroundUploading = true
|
this.backgroundUploading = true
|
||||||
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
this.$store.state.api.backendInteractor.updateProfileImages({ background }).then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
this.$store.commit('addNewUsers', [data])
|
this.$store.commit('addNewUsers', [data])
|
||||||
this.$store.commit('setCurrentUser', data)
|
this.$store.commit('setCurrentUser', data)
|
||||||
|
|
|
@ -13,8 +13,14 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner-background-preview {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
width: 300px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploading {
|
.uploading {
|
||||||
|
@ -26,18 +32,40 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg {
|
.current-avatar-container {
|
||||||
max-width: 100%;
|
position: relative;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-avatar {
|
.current-avatar {
|
||||||
display: block;
|
display: block;
|
||||||
width: 150px;
|
width: 100%;
|
||||||
height: 150px;
|
height: 100%;
|
||||||
border-radius: $fallback--avatarRadius;
|
border-radius: $fallback--avatarRadius;
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.2em;
|
||||||
|
right: 0.2em;
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
opacity: 0.7;
|
||||||
|
color: white;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.oauth-tokens {
|
.oauth-tokens {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -79,4 +107,22 @@
|
||||||
.setting-subitem {
|
.setting-subitem {
|
||||||
margin-left: 1.75em;
|
margin-left: 1.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-fields {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>.emoji-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 0 .2em .5em;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.icon-container {
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
&>.icon-cancel {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,54 @@
|
||||||
{{ $t('settings.discoverable') }}
|
{{ $t('settings.discoverable') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="maxFields > 0">
|
||||||
|
<p>{{ $t('settings.profile_fields.label') }}</p>
|
||||||
|
<div
|
||||||
|
v-for="(_, i) in newFields"
|
||||||
|
:key="i"
|
||||||
|
class="profile-fields"
|
||||||
|
>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newFields[i].name"
|
||||||
|
enable-emoji-picker
|
||||||
|
hide-emoji-button
|
||||||
|
:suggest="userSuggestor"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="newFields[i].name"
|
||||||
|
:placeholder="$t('settings.profile_fields.name')"
|
||||||
|
>
|
||||||
|
</EmojiInput>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newFields[i].value"
|
||||||
|
enable-emoji-picker
|
||||||
|
hide-emoji-button
|
||||||
|
:suggest="userSuggestor"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="newFields[i].value"
|
||||||
|
:placeholder="$t('settings.profile_fields.value')"
|
||||||
|
>
|
||||||
|
</EmojiInput>
|
||||||
|
<div
|
||||||
|
class="icon-container"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-show="newFields.length > 1"
|
||||||
|
class="icon-cancel"
|
||||||
|
@click="deleteField(i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="newFields.length < maxFields"
|
||||||
|
class="add-field faint"
|
||||||
|
@click="addField"
|
||||||
|
>
|
||||||
|
<i class="icon-plus" />
|
||||||
|
{{ $t("settings.profile_fields.add_field") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<Checkbox v-model="bot">
|
<Checkbox v-model="bot">
|
||||||
{{ $t('settings.bot') }}
|
{{ $t('settings.bot') }}
|
||||||
|
@ -113,11 +161,19 @@
|
||||||
<p class="visibility-notice">
|
<p class="visibility-notice">
|
||||||
{{ $t('settings.avatar_size_instruction') }}
|
{{ $t('settings.avatar_size_instruction') }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{ $t('settings.current_avatar') }}</p>
|
<div class="current-avatar-container">
|
||||||
<img
|
<img
|
||||||
:src="user.profile_image_url_original"
|
:src="user.profile_image_url_original"
|
||||||
class="current-avatar"
|
class="current-avatar"
|
||||||
>
|
>
|
||||||
|
<i
|
||||||
|
v-if="!isDefaultAvatar && pickAvatarBtnVisible"
|
||||||
|
:title="$t('settings.reset_avatar')"
|
||||||
|
class="reset-button icon-cancel"
|
||||||
|
type="button"
|
||||||
|
@click="resetAvatar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
<p>{{ $t('settings.set_new_avatar') }}</p>
|
||||||
<button
|
<button
|
||||||
v-show="pickAvatarBtnVisible"
|
v-show="pickAvatarBtnVisible"
|
||||||
|
@ -136,15 +192,20 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
<h2>{{ $t('settings.profile_banner') }}</h2>
|
||||||
<p>{{ $t('settings.current_profile_banner') }}</p>
|
<div class="banner-background-preview">
|
||||||
<img
|
<img :src="user.cover_photo">
|
||||||
:src="user.cover_photo"
|
<i
|
||||||
class="banner"
|
v-if="!isDefaultBanner"
|
||||||
>
|
:title="$t('settings.reset_profile_banner')"
|
||||||
|
class="reset-button icon-cancel"
|
||||||
|
type="button"
|
||||||
|
@click="resetBanner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
||||||
<img
|
<img
|
||||||
v-if="bannerPreview"
|
v-if="bannerPreview"
|
||||||
class="banner"
|
class="banner-background-preview"
|
||||||
:src="bannerPreview"
|
:src="bannerPreview"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -160,7 +221,7 @@
|
||||||
<button
|
<button
|
||||||
v-else-if="bannerPreview"
|
v-else-if="bannerPreview"
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
@click="submitBanner"
|
@click="submitBanner(banner)"
|
||||||
>
|
>
|
||||||
{{ $t('general.submit') }}
|
{{ $t('general.submit') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -177,10 +238,20 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
<h2>{{ $t('settings.profile_background') }}</h2>
|
||||||
|
<div class="banner-background-preview">
|
||||||
|
<img :src="user.background_image">
|
||||||
|
<i
|
||||||
|
v-if="!isDefaultBackground"
|
||||||
|
:title="$t('settings.reset_profile_background')"
|
||||||
|
class="reset-button icon-cancel"
|
||||||
|
type="button"
|
||||||
|
@click="resetBackground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
||||||
<img
|
<img
|
||||||
v-if="backgroundPreview"
|
v-if="backgroundPreview"
|
||||||
class="bg"
|
class="banner-background-preview"
|
||||||
:src="backgroundPreview"
|
:src="backgroundPreview"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -196,7 +267,7 @@
|
||||||
<button
|
<button
|
||||||
v-else-if="backgroundPreview"
|
v-else-if="backgroundPreview"
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
@click="submitBg"
|
@click="submitBackground(background)"
|
||||||
>
|
>
|
||||||
{{ $t('general.submit') }}
|
{{ $t('general.submit') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -99,7 +99,8 @@ export default {
|
||||||
avatarRadiusLocal: '',
|
avatarRadiusLocal: '',
|
||||||
avatarAltRadiusLocal: '',
|
avatarAltRadiusLocal: '',
|
||||||
attachmentRadiusLocal: '',
|
attachmentRadiusLocal: '',
|
||||||
tooltipRadiusLocal: ''
|
tooltipRadiusLocal: '',
|
||||||
|
chatMessageRadiusLocal: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -214,7 +215,8 @@ export default {
|
||||||
avatar: this.avatarRadiusLocal,
|
avatar: this.avatarRadiusLocal,
|
||||||
avatarAlt: this.avatarAltRadiusLocal,
|
avatarAlt: this.avatarAltRadiusLocal,
|
||||||
tooltip: this.tooltipRadiusLocal,
|
tooltip: this.tooltipRadiusLocal,
|
||||||
attachment: this.attachmentRadiusLocal
|
attachment: this.attachmentRadiusLocal,
|
||||||
|
chatMessage: this.chatMessageRadiusLocal
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preview () {
|
preview () {
|
||||||
|
|
|
@ -735,6 +735,65 @@
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="color-item">
|
||||||
|
<h4>{{ $t('chats.chats') }}</h4>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatBgColorLocal"
|
||||||
|
name="chatBgColor"
|
||||||
|
:fallback="previewTheme.colors.bg || 1"
|
||||||
|
:label="$t('settings.background')"
|
||||||
|
/>
|
||||||
|
<h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageIncomingBgColorLocal"
|
||||||
|
name="chatMessageIncomingBgColor"
|
||||||
|
:fallback="previewTheme.colors.bg || 1"
|
||||||
|
:label="$t('settings.background')"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageIncomingTextColorLocal"
|
||||||
|
name="chatMessageIncomingTextColor"
|
||||||
|
:fallback="previewTheme.colors.text || 1"
|
||||||
|
:label="$t('settings.text')"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageIncomingLinkColorLocal"
|
||||||
|
name="chatMessageIncomingLinkColor"
|
||||||
|
:fallback="previewTheme.colors.link || 1"
|
||||||
|
:label="$t('settings.links')"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageIncomingBorderColorLocal"
|
||||||
|
name="chatMessageIncomingBorderLinkColor"
|
||||||
|
:fallback="previewTheme.colors.fg || 1"
|
||||||
|
:label="$t('settings.style.advanced_colors.chat.border')"
|
||||||
|
/>
|
||||||
|
<h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageOutgoingBgColorLocal"
|
||||||
|
name="chatMessageOutgoingBgColor"
|
||||||
|
:fallback="previewTheme.colors.bg || 1"
|
||||||
|
:label="$t('settings.background')"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageOutgoingTextColorLocal"
|
||||||
|
name="chatMessageOutgoingTextColor"
|
||||||
|
:fallback="previewTheme.colors.text || 1"
|
||||||
|
:label="$t('settings.text')"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageOutgoingLinkColorLocal"
|
||||||
|
name="chatMessageOutgoingLinkColor"
|
||||||
|
:fallback="previewTheme.colors.link || 1"
|
||||||
|
:label="$t('settings.links')"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-model="chatMessageOutgoingBorderColorLocal"
|
||||||
|
name="chatMessageOutgoingBorderLinkColor"
|
||||||
|
:fallback="previewTheme.colors.bg || 1"
|
||||||
|
:label="$t('settings.style.advanced_colors.chat.border')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -814,6 +873,14 @@
|
||||||
max="50"
|
max="50"
|
||||||
hard-min="0"
|
hard-min="0"
|
||||||
/>
|
/>
|
||||||
|
<RangeInput
|
||||||
|
v-model="chatMessageRadiusLocal"
|
||||||
|
name="chatMessageRadius"
|
||||||
|
:label="$t('settings.chatMessageRadius')"
|
||||||
|
:fallback="previewTheme.radii.chatMessage || 2"
|
||||||
|
max="50"
|
||||||
|
hard-min="0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
@ -47,7 +48,11 @@ const SideDrawer = {
|
||||||
},
|
},
|
||||||
federating () {
|
federating () {
|
||||||
return this.$store.state.instance.federating
|
return this.$store.state.instance.federating
|
||||||
}
|
},
|
||||||
|
...mapState({
|
||||||
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||||
|
}),
|
||||||
|
...mapGetters(['unreadChatCount'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleDrawer () {
|
toggleDrawer () {
|
||||||
|
|
|
@ -40,12 +40,24 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser"
|
v-if="currentUser && pleromaChatMessagesAvailable"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
||||||
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
|
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
|
||||||
|
style="position: relative"
|
||||||
|
>
|
||||||
|
<i class="button-icon icon-chat" /> {{ $t("nav.chats") }}
|
||||||
|
<span
|
||||||
|
v-if="unreadChatCount"
|
||||||
|
class="badge badge-notification unread-chat-count"
|
||||||
|
>
|
||||||
|
{{ unreadChatCount }}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
|
@ -65,6 +77,14 @@
|
||||||
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
|
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="currentUser"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: 'bookmarks'}">
|
||||||
|
<i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser && currentUser.locked"
|
v-if="currentUser && currentUser.locked"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
|
@ -95,14 +115,6 @@
|
||||||
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
|
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
|
||||||
v-if="currentUser && chat"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
>
|
|
||||||
<router-link :to="{ name: 'chat' }">
|
|
||||||
<i class="button-icon icon-chat" /> {{ $t("nav.chat") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -2,6 +2,10 @@ import map from 'lodash/map'
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
|
||||||
const StaffPanel = {
|
const StaffPanel = {
|
||||||
|
created () {
|
||||||
|
const nicknames = this.$store.state.instance.staffAccounts
|
||||||
|
nicknames.forEach(nickname => this.$store.dispatch('fetchUserIfMissing', nickname))
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
BasicUserCard
|
BasicUserCard
|
||||||
},
|
},
|
||||||
|
|
|
@ -141,7 +141,7 @@ const Status = {
|
||||||
return this.mergedConfig.hideFilteredStatuses
|
return this.mergedConfig.hideFilteredStatuses
|
||||||
},
|
},
|
||||||
hideStatus () {
|
hideStatus () {
|
||||||
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
|
return this.deleted || (this.muted && this.hideFilteredStatuses)
|
||||||
},
|
},
|
||||||
isFocused () {
|
isFocused () {
|
||||||
// retweet or root of an expanded conversation
|
// retweet or root of an expanded conversation
|
||||||
|
@ -164,37 +164,6 @@ const Status = {
|
||||||
return user && user.screen_name
|
return user && user.screen_name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideReply () {
|
|
||||||
if (this.mergedConfig.replyVisibility === 'all') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.inConversation || !this.isReply) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.user.id === this.currentUser.id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.type === 'retweet') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const checkFollowing = this.mergedConfig.replyVisibility === 'following'
|
|
||||||
for (var i = 0; i < this.status.attentions.length; ++i) {
|
|
||||||
if (this.status.user.id === this.status.attentions[i].id) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// There's zero guarantee of this working. If we happen to have that user and their
|
|
||||||
// relationship in store then it will work, but there's kinda little chance of having
|
|
||||||
// them for people you're not following.
|
|
||||||
const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]
|
|
||||||
if (checkFollowing && relationship && relationship.following) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.attentions[i].id === this.currentUser.id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.status.attentions.length > 0
|
|
||||||
},
|
|
||||||
replySubject () {
|
replySubject () {
|
||||||
if (!this.status.summary) return ''
|
if (!this.status.summary) return ''
|
||||||
const decodedSummary = unescape(this.status.summary)
|
const decodedSummary = unescape(this.status.summary)
|
||||||
|
|
|
@ -197,7 +197,7 @@
|
||||||
>
|
>
|
||||||
<StatusPopover
|
<StatusPopover
|
||||||
v-if="!isPreview"
|
v-if="!isPreview"
|
||||||
:status-id="status.in_reply_to_status_id"
|
:status-id="status.parent_visible && status.in_reply_to_status_id"
|
||||||
class="reply-to-popover"
|
class="reply-to-popover"
|
||||||
style="min-width: 0"
|
style="min-width: 0"
|
||||||
>
|
>
|
||||||
|
@ -208,7 +208,12 @@
|
||||||
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
||||||
>
|
>
|
||||||
<i class="button-icon icon-reply" />
|
<i class="button-icon icon-reply" />
|
||||||
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
|
<span
|
||||||
|
class="faint-link reply-to-text"
|
||||||
|
:class="{ 'strikethrough': !status.parent_visible }"
|
||||||
|
>
|
||||||
|
{{ $t('status.reply_to') }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</StatusPopover>
|
</StatusPopover>
|
||||||
<span
|
<span
|
||||||
|
@ -372,9 +377,6 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-el {
|
.status-el {
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
border-left-width: 0px;
|
border-left-width: 0px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
|
@ -526,6 +528,10 @@ $status-margin: 0.75em;
|
||||||
margin: 0 0.4em 0 0.2em;
|
margin: 0 0.4em 0 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.replies-separator {
|
.replies-separator {
|
||||||
margin-left: 0.4em;
|
margin-left: 0.4em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,12 @@ const StatusContent = {
|
||||||
'status',
|
'status',
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent'
|
'fullContent',
|
||||||
|
'singleLine'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showingTall: this.inConversation && this.focused,
|
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||||
showingLongSubject: false,
|
showingLongSubject: false,
|
||||||
// not as computed because it sets the initial state which will be changed later
|
// not as computed because it sets the initial state which will be changed later
|
||||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||||
|
@ -44,14 +45,14 @@ const StatusContent = {
|
||||||
return lengthScore > 20
|
return lengthScore > 20
|
||||||
},
|
},
|
||||||
longSubject () {
|
longSubject () {
|
||||||
return this.status.summary.length > 900
|
return this.status.summary.length > 240
|
||||||
},
|
},
|
||||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||||
mightHideBecauseSubject () {
|
mightHideBecauseSubject () {
|
||||||
return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
|
return !!this.status.summary && this.localCollapseSubjectDefault
|
||||||
},
|
},
|
||||||
mightHideBecauseTall () {
|
mightHideBecauseTall () {
|
||||||
return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
|
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
|
||||||
},
|
},
|
||||||
hideSubjectStatus () {
|
hideSubjectStatus () {
|
||||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||||
|
@ -99,15 +100,8 @@ const StatusContent = {
|
||||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
hasImageAttachments () {
|
attachmentTypes () {
|
||||||
return this.status.attachments.some(
|
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||||
file => fileType.fileType(file.mimetype) === 'image'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
hasVideoAttachments () {
|
|
||||||
return this.status.attachments.some(
|
|
||||||
file => fileType.fileType(file.mimetype) === 'video'
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
maxThumbnails () {
|
maxThumbnails () {
|
||||||
return this.mergedConfig.maxThumbnails
|
return this.mergedConfig.maxThumbnails
|
||||||
|
@ -142,12 +136,6 @@ const StatusContent = {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentHtml () {
|
|
||||||
if (!this.status.summary_html) {
|
|
||||||
return this.postBodyHtml
|
|
||||||
}
|
|
||||||
return this.status.summary_html + '<br />' + this.postBodyHtml
|
|
||||||
},
|
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
betterShadow: state => state.interface.browserSupport.cssFilter,
|
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||||
|
|
|
@ -3,45 +3,32 @@
|
||||||
<div class="status-body">
|
<div class="status-body">
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
<div
|
<div
|
||||||
v-if="longSubject"
|
v-if="status.summary_html"
|
||||||
class="status-content-wrapper"
|
class="summary-wrapper"
|
||||||
:class="{ 'tall-status': !showingLongSubject }"
|
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="media-body summary"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="status.summary_html"
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
v-if="!showingLongSubject"
|
v-if="longSubject && showingLongSubject"
|
||||||
class="tall-status-hider"
|
href="#"
|
||||||
:class="{ 'tall-status-hider_focused': focused }"
|
class="tall-subject-hider"
|
||||||
|
@click.prevent="showingLongSubject=false"
|
||||||
|
>{{ $t("status.hide_full_subject") }}</a>
|
||||||
|
<a
|
||||||
|
v-else-if="longSubject"
|
||||||
|
class="tall-subject-hider"
|
||||||
|
:class="{ 'tall-subject-hider_focused': focused }"
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="showingLongSubject=true"
|
@click.prevent="showingLongSubject=true"
|
||||||
>
|
>
|
||||||
{{ $t("general.show_more") }}
|
{{ $t("status.show_full_subject") }}
|
||||||
<span
|
|
||||||
v-if="hasImageAttachments"
|
|
||||||
class="icon-picture"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="hasVideoAttachments"
|
|
||||||
class="icon-video"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="status.card"
|
|
||||||
class="icon-link"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
<div
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="contentHtml"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="showingLongSubject"
|
|
||||||
href="#"
|
|
||||||
class="status-unhider"
|
|
||||||
@click.prevent="showingLongSubject=false"
|
|
||||||
>{{ $t("general.show_less") }}</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
|
||||||
:class="{'tall-status': hideTallStatus}"
|
:class="{'tall-status': hideTallStatus}"
|
||||||
class="status-content-wrapper"
|
class="status-content-wrapper"
|
||||||
>
|
>
|
||||||
|
@ -51,31 +38,52 @@
|
||||||
:class="{ 'tall-status-hider_focused': focused }"
|
:class="{ 'tall-status-hider_focused': focused }"
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="toggleShowMore"
|
@click.prevent="toggleShowMore"
|
||||||
>{{ $t("general.show_more") }}</a>
|
>
|
||||||
|
{{ $t("general.show_more") }}
|
||||||
|
</a>
|
||||||
<div
|
<div
|
||||||
v-if="!hideSubjectStatus"
|
v-if="!hideSubjectStatus"
|
||||||
|
:class="{ 'single-line': singleLine }"
|
||||||
class="status-content media-body"
|
class="status-content media-body"
|
||||||
@click.prevent="linkClicked"
|
@click.prevent="linkClicked"
|
||||||
v-html="contentHtml"
|
v-html="postBodyHtml"
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="status.summary_html"
|
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
v-if="hideSubjectStatus"
|
v-if="hideSubjectStatus"
|
||||||
href="#"
|
href="#"
|
||||||
class="cw-status-hider"
|
class="cw-status-hider"
|
||||||
@click.prevent="toggleShowMore"
|
@click.prevent="toggleShowMore"
|
||||||
>{{ $t("general.show_more") }}</a>
|
>
|
||||||
|
{{ $t("status.show_content") }}
|
||||||
|
<span
|
||||||
|
v-if="attachmentTypes.includes('image')"
|
||||||
|
class="icon-picture"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="attachmentTypes.includes('video')"
|
||||||
|
class="icon-video"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="attachmentTypes.includes('audio')"
|
||||||
|
class="icon-music"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="attachmentTypes.includes('unknown')"
|
||||||
|
class="icon-doc"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="status.card"
|
||||||
|
class="icon-link"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="showingMore"
|
v-if="showingMore && !fullContent"
|
||||||
href="#"
|
href="#"
|
||||||
class="status-unhider"
|
class="status-unhider"
|
||||||
@click.prevent="toggleShowMore"
|
@click.prevent="toggleShowMore"
|
||||||
>{{ $t("general.show_less") }}</a>
|
>
|
||||||
|
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="status.poll && status.poll.options">
|
<div v-if="status.poll && status.poll.options">
|
||||||
|
@ -129,6 +137,12 @@ $status-margin: 0.75em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
.status-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.tall-status {
|
.tall-status {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 220px;
|
height: 220px;
|
||||||
|
@ -136,7 +150,7 @@ $status-margin: 0.75em;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
.status-content {
|
.status-content {
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||||
linear-gradient(to top, white, white);
|
linear-gradient(to top, white, white);
|
||||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||||
|
@ -176,10 +190,45 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-wrapper {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
font-style: italic;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tall-subject {
|
||||||
|
position: relative;
|
||||||
|
.summary {
|
||||||
|
max-height: 2em;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tall-subject-hider {
|
||||||
|
display: inline-block;
|
||||||
|
word-break: break-all;
|
||||||
|
// position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.status-content {
|
.status-content {
|
||||||
font-family: var(--postFont, sans-serif);
|
font-family: var(--postFont, sans-serif);
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 0.2em 0 0.2em 2em;
|
margin: 0.2em 0 0.2em 2em;
|
||||||
|
@ -221,6 +270,12 @@ $status-margin: 0.75em;
|
||||||
h4 {
|
h4 {
|
||||||
margin: 1.1em 0;
|
margin: 1.1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.single-line {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@ const StatusPopover = {
|
||||||
methods: {
|
methods: {
|
||||||
enter () {
|
enter () {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
|
if (!this.statusId) {
|
||||||
|
this.error = true
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$store.dispatch('fetchStatus', this.statusId)
|
this.$store.dispatch('fetchStatus', this.statusId)
|
||||||
.then(data => (this.error = false))
|
.then(data => (this.error = false))
|
||||||
.catch(e => (this.error = true))
|
.catch(e => (this.error = true))
|
||||||
|
|
|
@ -4,7 +4,8 @@ const StillImage = {
|
||||||
'referrerpolicy',
|
'referrerpolicy',
|
||||||
'mimetype',
|
'mimetype',
|
||||||
'imageLoadError',
|
'imageLoadError',
|
||||||
'imageLoadHandler'
|
'imageLoadHandler',
|
||||||
|
'alt'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
<img
|
<img
|
||||||
ref="src"
|
ref="src"
|
||||||
:key="src"
|
:key="src"
|
||||||
|
:alt="alt"
|
||||||
|
:title="alt"
|
||||||
:src="src"
|
:src="src"
|
||||||
:referrerpolicy="referrerpolicy"
|
:referrerpolicy="referrerpolicy"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
|
|
|
@ -45,11 +45,15 @@ const Timeline = {
|
||||||
newStatusCount () {
|
newStatusCount () {
|
||||||
return this.timeline.newStatusCount
|
return this.timeline.newStatusCount
|
||||||
},
|
},
|
||||||
newStatusCountStr () {
|
showLoadButton () {
|
||||||
|
if (this.timelineError || this.errorData) return false
|
||||||
|
return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
|
||||||
|
},
|
||||||
|
loadButtonString () {
|
||||||
if (this.timeline.flushMarker !== 0) {
|
if (this.timeline.flushMarker !== 0) {
|
||||||
return ''
|
return this.$t('timeline.reload')
|
||||||
} else {
|
} else {
|
||||||
return ` (${this.newStatusCount})`
|
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
classes () {
|
classes () {
|
||||||
|
@ -112,8 +116,6 @@ const Timeline = {
|
||||||
if (e.key === '.') this.showNewStatuses()
|
if (e.key === '.') this.showNewStatuses()
|
||||||
},
|
},
|
||||||
showNewStatuses () {
|
showNewStatuses () {
|
||||||
if (this.newStatusCount === 0) return
|
|
||||||
|
|
||||||
if (this.timeline.flushMarker !== 0) {
|
if (this.timeline.flushMarker !== 0) {
|
||||||
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
|
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
|
||||||
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
||||||
|
@ -135,7 +137,7 @@ const Timeline = {
|
||||||
showImmediately: true,
|
showImmediately: true,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
tag: this.tag
|
tag: this.tag
|
||||||
}).then(statuses => {
|
}).then(({ statuses }) => {
|
||||||
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||||
if (statuses && statuses.length === 0) {
|
if (statuses && statuses.length === 0) {
|
||||||
this.bottomedOut = true
|
this.bottomedOut = true
|
||||||
|
|
|
@ -19,14 +19,14 @@
|
||||||
{{ errorData.statusText }}
|
{{ errorData.statusText }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="timeline.newStatusCount > 0 && !timelineError && !errorData"
|
v-else-if="showLoadButton"
|
||||||
class="loadmore-button"
|
class="loadmore-button"
|
||||||
@click.prevent="showNewStatuses"
|
@click.prevent="showNewStatuses"
|
||||||
>
|
>
|
||||||
{{ $t('timeline.show_new') }}{{ newStatusCountStr }}
|
{{ loadButtonString }}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData"
|
v-else
|
||||||
class="loadmore-text faint"
|
class="loadmore-text faint"
|
||||||
@click.prevent
|
@click.prevent
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,26 +8,20 @@ const UserAvatar = {
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showPlaceholder: false
|
showPlaceholder: false,
|
||||||
|
defaultAvatar: `${this.$store.state.instance.server + this.$store.state.instance.defaultAvatar}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StillImage
|
StillImage
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
imgSrc () {
|
|
||||||
return this.showPlaceholder ? '/images/avi.png' : this.user.profile_image_url_original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
|
imgSrc (src) {
|
||||||
|
return (!src || this.showPlaceholder) ? this.defaultAvatar : src
|
||||||
|
},
|
||||||
imageLoadError () {
|
imageLoadError () {
|
||||||
this.showPlaceholder = true
|
this.showPlaceholder = true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
src () {
|
|
||||||
this.showPlaceholder = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:alt="user.screen_name"
|
:alt="user.screen_name"
|
||||||
:title="user.screen_name"
|
:title="user.screen_name"
|
||||||
:src="imgSrc"
|
:src="imgSrc(user.profile_image_url_original)"
|
||||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||||
:image-load-error="imageLoadError"
|
:image-load-error="imageLoadError"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,9 +10,7 @@
|
||||||
:hide-bio="true"
|
:hide-bio="true"
|
||||||
rounded="top"
|
rounded="top"
|
||||||
/>
|
/>
|
||||||
<div class="panel-footer">
|
<PostStatusForm />
|
||||||
<PostStatusForm />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<auth-form
|
<auth-form
|
||||||
v-else
|
v-else
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
:src="attachment.url"
|
:src="attachment.url"
|
||||||
:loop="loopVideo"
|
:loop="loopVideo"
|
||||||
:controls="controls"
|
:controls="controls"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
playsinline
|
playsinline
|
||||||
@loadeddata="onVideoDataLoad"
|
@loadeddata="onVideoDataLoad"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,7 +7,7 @@ function showWhoToFollow (panel, reply) {
|
||||||
|
|
||||||
panel.usersToFollow.forEach((toFollow, index) => {
|
panel.usersToFollow.forEach((toFollow, index) => {
|
||||||
let user = shuffled[index]
|
let user = shuffled[index]
|
||||||
let img = user.avatar || '/images/avi.png'
|
let img = user.avatar || this.$store.state.instance.defaultAvatar
|
||||||
let name = user.acct
|
let name = user.acct
|
||||||
|
|
||||||
toFollow.img = img
|
toFollow.img = img
|
||||||
|
@ -38,13 +38,7 @@ function getWhoToFollow (panel) {
|
||||||
|
|
||||||
const WhoToFollowPanel = {
|
const WhoToFollowPanel = {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
usersToFollow: new Array(3).fill().map(x => (
|
usersToFollow: []
|
||||||
{
|
|
||||||
img: '/images/avi.png',
|
|
||||||
name: '',
|
|
||||||
id: 0
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
user: function () {
|
user: function () {
|
||||||
|
@ -68,6 +62,13 @@ const WhoToFollowPanel = {
|
||||||
},
|
},
|
||||||
mounted:
|
mounted:
|
||||||
function () {
|
function () {
|
||||||
|
this.usersToFollow = new Array(3).fill().map(x => (
|
||||||
|
{
|
||||||
|
img: this.$store.state.instance.defaultAvatar,
|
||||||
|
name: '',
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
))
|
||||||
if (this.suggestionsEnabled) {
|
if (this.suggestionsEnabled) {
|
||||||
getWhoToFollow(this)
|
getWhoToFollow(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,9 @@
|
||||||
.error {
|
.error {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
},
|
},
|
||||||
"features_panel": {
|
"features_panel": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"pleroma_chat_messages": "Pleroma Chat",
|
||||||
"gopher": "Gopher",
|
"gopher": "Gopher",
|
||||||
"media_proxy": "Media proxy",
|
"media_proxy": "Media proxy",
|
||||||
"scope_options": "Scope options",
|
"scope_options": "Scope options",
|
||||||
|
@ -120,10 +121,12 @@
|
||||||
"public_tl": "Public Timeline",
|
"public_tl": "Public Timeline",
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"twkn": "The Whole Known Network",
|
"twkn": "The Whole Known Network",
|
||||||
|
"bookmarks": "Bookmarks",
|
||||||
"user_search": "User Search",
|
"user_search": "User Search",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"who_to_follow": "Who to follow",
|
"who_to_follow": "Who to follow",
|
||||||
"preferences": "Preferences"
|
"preferences": "Preferences",
|
||||||
|
"chats": "Chats"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Unknown status, searching for it…",
|
"broken_favorite": "Unknown status, searching for it…",
|
||||||
|
@ -163,6 +166,9 @@
|
||||||
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
|
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
|
||||||
"load_all": "Loading all {emojiAmount} emoji"
|
"load_all": "Loading all {emojiAmount} emoji"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."
|
||||||
|
},
|
||||||
"interactions": {
|
"interactions": {
|
||||||
"favs_repeats": "Repeats and Favorites",
|
"favs_repeats": "Repeats and Favorites",
|
||||||
"follows": "New follows",
|
"follows": "New follows",
|
||||||
|
@ -174,6 +180,7 @@
|
||||||
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
|
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
|
||||||
"account_not_locked_warning_link": "locked",
|
"account_not_locked_warning_link": "locked",
|
||||||
"attachments_sensitive": "Mark attachments as sensitive",
|
"attachments_sensitive": "Mark attachments as sensitive",
|
||||||
|
"media_description": "Media description",
|
||||||
"content_type": {
|
"content_type": {
|
||||||
"text/plain": "Plain text",
|
"text/plain": "Plain text",
|
||||||
"text/html": "HTML",
|
"text/html": "HTML",
|
||||||
|
@ -185,6 +192,10 @@
|
||||||
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
||||||
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
||||||
"posting": "Posting",
|
"posting": "Posting",
|
||||||
|
"preview": "Preview",
|
||||||
|
"preview_empty": "Empty",
|
||||||
|
"empty_status_error": "Can't post an empty status with no files",
|
||||||
|
"media_description_error": "Failed to update media, try again",
|
||||||
"scope_notice": {
|
"scope_notice": {
|
||||||
"public": "This post will be visible to everyone",
|
"public": "This post will be visible to everyone",
|
||||||
"private": "This post will be visible to your followers only",
|
"private": "This post will be visible to your followers only",
|
||||||
|
@ -278,12 +289,11 @@
|
||||||
"change_password": "Change Password",
|
"change_password": "Change Password",
|
||||||
"change_password_error": "There was an issue changing your password.",
|
"change_password_error": "There was an issue changing your password.",
|
||||||
"changed_password": "Password changed successfully!",
|
"changed_password": "Password changed successfully!",
|
||||||
|
"chatMessageRadius": "Chat message",
|
||||||
"collapse_subject": "Collapse posts with subjects",
|
"collapse_subject": "Collapse posts with subjects",
|
||||||
"composing": "Composing",
|
"composing": "Composing",
|
||||||
"confirm_new_password": "Confirm new password",
|
"confirm_new_password": "Confirm new password",
|
||||||
"current_avatar": "Your current avatar",
|
|
||||||
"current_password": "Current password",
|
"current_password": "Current password",
|
||||||
"current_profile_banner": "Your current profile banner",
|
|
||||||
"mutes_and_blocks": "Mutes and Blocks",
|
"mutes_and_blocks": "Mutes and Blocks",
|
||||||
"data_import_export_tab": "Data Import / Export",
|
"data_import_export_tab": "Data Import / Export",
|
||||||
"default_vis": "Default visibility scope",
|
"default_vis": "Default visibility scope",
|
||||||
|
@ -334,6 +344,12 @@
|
||||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||||
"mutes_tab": "Mutes",
|
"mutes_tab": "Mutes",
|
||||||
"play_videos_in_modal": "Play videos in a popup frame",
|
"play_videos_in_modal": "Play videos in a popup frame",
|
||||||
|
"profile_fields": {
|
||||||
|
"label": "Profile metadata",
|
||||||
|
"add_field": "Add Field",
|
||||||
|
"name": "Label",
|
||||||
|
"value": "Content"
|
||||||
|
},
|
||||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_bio": "Name & Bio",
|
"name_bio": "Name & Bio",
|
||||||
|
@ -384,6 +400,12 @@
|
||||||
"set_new_avatar": "Set new avatar",
|
"set_new_avatar": "Set new avatar",
|
||||||
"set_new_profile_background": "Set new profile background",
|
"set_new_profile_background": "Set new profile background",
|
||||||
"set_new_profile_banner": "Set new profile banner",
|
"set_new_profile_banner": "Set new profile banner",
|
||||||
|
"reset_avatar": "Reset avatar",
|
||||||
|
"reset_profile_background": "Reset profile background",
|
||||||
|
"reset_profile_banner": "Reset profile banner",
|
||||||
|
"reset_avatar_confirm": "Do you really want to reset the avatar?",
|
||||||
|
"reset_banner_confirm": "Do you really want to reset the banner?",
|
||||||
|
"reset_background_confirm": "Do you really want to reset the background?",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"subject_input_always_show": "Always show subject field",
|
"subject_input_always_show": "Always show subject field",
|
||||||
"subject_line_behavior": "Copy subject when replying",
|
"subject_line_behavior": "Copy subject when replying",
|
||||||
|
@ -496,7 +518,12 @@
|
||||||
"selectedMenu": "Selected menu item",
|
"selectedMenu": "Selected menu item",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"toggled": "Toggled",
|
"toggled": "Toggled",
|
||||||
"tabs": "Tabs"
|
"tabs": "Tabs",
|
||||||
|
"chat": {
|
||||||
|
"incoming": "Incoming",
|
||||||
|
"outgoing": "Outgoing",
|
||||||
|
"border": "Border"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"radii": {
|
"radii": {
|
||||||
"_tab_label": "Roundness"
|
"_tab_label": "Roundness"
|
||||||
|
@ -608,6 +635,7 @@
|
||||||
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
|
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
|
||||||
"repeated": "repeated",
|
"repeated": "repeated",
|
||||||
"show_new": "Show new",
|
"show_new": "Show new",
|
||||||
|
"reload": "Reload",
|
||||||
"up_to_date": "Up-to-date",
|
"up_to_date": "Up-to-date",
|
||||||
"no_more_statuses": "No more statuses",
|
"no_more_statuses": "No more statuses",
|
||||||
"no_statuses": "No statuses"
|
"no_statuses": "No statuses"
|
||||||
|
@ -619,6 +647,8 @@
|
||||||
"pin": "Pin on profile",
|
"pin": "Pin on profile",
|
||||||
"unpin": "Unpin from profile",
|
"unpin": "Unpin from profile",
|
||||||
"pinned": "Pinned",
|
"pinned": "Pinned",
|
||||||
|
"bookmark": "Bookmark",
|
||||||
|
"unbookmark": "Unbookmark",
|
||||||
"delete_confirm": "Do you really want to delete this status?",
|
"delete_confirm": "Do you really want to delete this status?",
|
||||||
"reply_to": "Reply to",
|
"reply_to": "Reply to",
|
||||||
"replies_list": "Replies:",
|
"replies_list": "Replies:",
|
||||||
|
@ -627,7 +657,11 @@
|
||||||
"status_unavailable": "Status unavailable",
|
"status_unavailable": "Status unavailable",
|
||||||
"copy_link": "Copy link to status",
|
"copy_link": "Copy link to status",
|
||||||
"thread_muted": "Thread muted",
|
"thread_muted": "Thread muted",
|
||||||
"thread_muted_and_words": ", has words:"
|
"thread_muted_and_words": ", has words:",
|
||||||
|
"show_full_subject": "Show full subject",
|
||||||
|
"hide_full_subject": "Hide full subject",
|
||||||
|
"show_content": "Show content",
|
||||||
|
"hide_content": "Hide content"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
@ -648,6 +682,7 @@
|
||||||
"its_you": "It's you!",
|
"its_you": "It's you!",
|
||||||
"media": "Media",
|
"media": "Media",
|
||||||
"mention": "Mention",
|
"mention": "Mention",
|
||||||
|
"message": "Message",
|
||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"muted": "Muted",
|
"muted": "Muted",
|
||||||
"per_day": "per day",
|
"per_day": "per day",
|
||||||
|
@ -710,7 +745,8 @@
|
||||||
"add_reaction": "Add Reaction",
|
"add_reaction": "Add Reaction",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"accept_follow_request": "Accept follow request",
|
"accept_follow_request": "Accept follow request",
|
||||||
"reject_follow_request": "Reject follow request"
|
"reject_follow_request": "Reject follow request",
|
||||||
|
"bookmark": "Bookmark"
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"error": {
|
"error": {
|
||||||
|
@ -745,5 +781,27 @@
|
||||||
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
|
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
|
||||||
"password_reset_required": "You must reset your password to log in.",
|
"password_reset_required": "You must reset your password to log in.",
|
||||||
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
|
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
|
||||||
|
},
|
||||||
|
"chats": {
|
||||||
|
"you": "You:",
|
||||||
|
"message_user": "Message {nickname}",
|
||||||
|
"delete": "Delete",
|
||||||
|
"chats": "Chats",
|
||||||
|
"new": "New Chat",
|
||||||
|
"empty_message_error": "Cannot post empty message",
|
||||||
|
"more": "More",
|
||||||
|
"delete_confirm": "Do you really want to delete this message?",
|
||||||
|
"error_loading_chat": "Something went wrong when loading the chat.",
|
||||||
|
"error_sending_message": "Something went wrong when sending the message.",
|
||||||
|
"empty_chat_list_placeholder": "You don't have any chats yet. Start a new chat!"
|
||||||
|
},
|
||||||
|
"file_type": {
|
||||||
|
"audio": "Audio",
|
||||||
|
"video": "Video",
|
||||||
|
"image": "Image",
|
||||||
|
"file": "File"
|
||||||
|
},
|
||||||
|
"display_date": {
|
||||||
|
"today": "Today"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,12 @@
|
||||||
"disable": "Poista käytöstä",
|
"disable": "Poista käytöstä",
|
||||||
"confirm": "Hyväksy",
|
"confirm": "Hyväksy",
|
||||||
"verify": "Varmenna",
|
"verify": "Varmenna",
|
||||||
"enable": "Ota käyttöön"
|
"enable": "Ota käyttöön",
|
||||||
|
"loading": "Ladataan…",
|
||||||
|
"error_retry": "Yritä uudelleen",
|
||||||
|
"retry": "Yritä uudelleen",
|
||||||
|
"close": "Sulje",
|
||||||
|
"peek": "Kurkkaa"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "Kirjaudu sisään",
|
"login": "Kirjaudu sisään",
|
||||||
|
@ -63,10 +68,11 @@
|
||||||
"who_to_follow": "Seurausehdotukset",
|
"who_to_follow": "Seurausehdotukset",
|
||||||
"preferences": "Asetukset",
|
"preferences": "Asetukset",
|
||||||
"administration": "Ylläpito",
|
"administration": "Ylläpito",
|
||||||
"search": "Haku"
|
"search": "Haku",
|
||||||
|
"bookmarks": "Kirjanmerkit"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Viestiä ei löydetty...",
|
"broken_favorite": "Viestiä ei löydetty…",
|
||||||
"favorited_you": "tykkäsi viestistäsi",
|
"favorited_you": "tykkäsi viestistäsi",
|
||||||
"followed_you": "seuraa sinua",
|
"followed_you": "seuraa sinua",
|
||||||
"load_older": "Lataa vanhempia ilmoituksia",
|
"load_older": "Lataa vanhempia ilmoituksia",
|
||||||
|
@ -101,7 +107,7 @@
|
||||||
},
|
},
|
||||||
"post_status": {
|
"post_status": {
|
||||||
"new_status": "Uusi viesti",
|
"new_status": "Uusi viesti",
|
||||||
"account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi",
|
"account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi.",
|
||||||
"account_not_locked_warning_link": "lukittu",
|
"account_not_locked_warning_link": "lukittu",
|
||||||
"attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi",
|
"attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi",
|
||||||
"content_type": {
|
"content_type": {
|
||||||
|
@ -126,7 +132,12 @@
|
||||||
"public": "Tämä viesti näkyy kaikille",
|
"public": "Tämä viesti näkyy kaikille",
|
||||||
"private": "Tämä viesti näkyy vain sinun seuraajillesi",
|
"private": "Tämä viesti näkyy vain sinun seuraajillesi",
|
||||||
"unlisted": "Tämä viesti ei näy Julkisella Aikajanalla tai Koko Tunnettu Verkosto -aikajanalla"
|
"unlisted": "Tämä viesti ei näy Julkisella Aikajanalla tai Koko Tunnettu Verkosto -aikajanalla"
|
||||||
}
|
},
|
||||||
|
"preview": "Esikatselu",
|
||||||
|
"preview_empty": "Tyhjä",
|
||||||
|
"empty_status_error": "Tyhjää viestiä ilman tiedostoja ei voi lähettää",
|
||||||
|
"media_description": "Tiedoston kuvaus",
|
||||||
|
"media_description_error": "Tiedostojen päivitys epäonnistui, yritä uudelleen"
|
||||||
},
|
},
|
||||||
"registration": {
|
"registration": {
|
||||||
"bio": "Kuvaus",
|
"bio": "Kuvaus",
|
||||||
|
@ -175,7 +186,7 @@
|
||||||
"data_import_export_tab": "Tietojen tuonti / vienti",
|
"data_import_export_tab": "Tietojen tuonti / vienti",
|
||||||
"default_vis": "Oletusnäkyvyysrajaus",
|
"default_vis": "Oletusnäkyvyysrajaus",
|
||||||
"delete_account": "Poista tili",
|
"delete_account": "Poista tili",
|
||||||
"delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
|
"delete_account_description": "Poista tietosi ja lukitse tili pysyvästi.",
|
||||||
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
|
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
|
||||||
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
|
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
|
||||||
"emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
|
"emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
|
||||||
|
@ -288,7 +299,7 @@
|
||||||
"authentication_methods": "Todennus",
|
"authentication_methods": "Todennus",
|
||||||
"warning_of_generate_new_codes": "Luodessasi uudet palautuskoodit, vanhat koodisi lakkaavat toimimasta.",
|
"warning_of_generate_new_codes": "Luodessasi uudet palautuskoodit, vanhat koodisi lakkaavat toimimasta.",
|
||||||
"recovery_codes": "Palautuskoodit.",
|
"recovery_codes": "Palautuskoodit.",
|
||||||
"waiting_a_recovery_codes": "Odotetaan palautuskoodeja...",
|
"waiting_a_recovery_codes": "Odotetaan palautuskoodeja…",
|
||||||
"recovery_codes_warning": "Kirjoita koodit ylös tai tallenna ne turvallisesti, muuten et näe niitä uudestaan. Jos et voi käyttää monivaihetodennusta ja sinulla ei ole palautuskoodeja, et voi enää kirjautua sisään tilillesi.",
|
"recovery_codes_warning": "Kirjoita koodit ylös tai tallenna ne turvallisesti, muuten et näe niitä uudestaan. Jos et voi käyttää monivaihetodennusta ja sinulla ei ole palautuskoodeja, et voi enää kirjautua sisään tilillesi.",
|
||||||
"scan": {
|
"scan": {
|
||||||
"title": "Skannaa",
|
"title": "Skannaa",
|
||||||
|
@ -329,7 +340,7 @@
|
||||||
"post_status_content_type": "Uuden viestin sisällön muoto",
|
"post_status_content_type": "Uuden viestin sisällön muoto",
|
||||||
"user_mutes": "Käyttäjät",
|
"user_mutes": "Käyttäjät",
|
||||||
"useStreamingApiWarning": "(Kokeellinen)",
|
"useStreamingApiWarning": "(Kokeellinen)",
|
||||||
"type_domains_to_mute": "Syötä mykistettäviä sivustoja",
|
"type_domains_to_mute": "Etsi mykistettäviä sivustoja",
|
||||||
"upload_a_photo": "Lataa kuva",
|
"upload_a_photo": "Lataa kuva",
|
||||||
"fun": "Hupi",
|
"fun": "Hupi",
|
||||||
"greentext": "Meeminuolet",
|
"greentext": "Meeminuolet",
|
||||||
|
@ -490,7 +501,21 @@
|
||||||
"title": "Versio",
|
"title": "Versio",
|
||||||
"backend_version": "Palvelimen versio",
|
"backend_version": "Palvelimen versio",
|
||||||
"frontend_version": "Käyttöliittymän versio"
|
"frontend_version": "Käyttöliittymän versio"
|
||||||
}
|
},
|
||||||
|
"reset_profile_background": "Nollaa taustakuva",
|
||||||
|
"reset_background_confirm": "Haluatko todella nollata taustakuvan?",
|
||||||
|
"mutes_and_blocks": "Mykistykset ja Estot",
|
||||||
|
"bot": "Tämä on bottitili",
|
||||||
|
"profile_fields": {
|
||||||
|
"label": "Profiilin metatiedot",
|
||||||
|
"add_field": "Lisää kenttä",
|
||||||
|
"name": "Nimi",
|
||||||
|
"value": "Sisältö"
|
||||||
|
},
|
||||||
|
"reset_avatar": "Nollaa profiilikuva",
|
||||||
|
"reset_profile_banner": "Nollaa profiilin tausta",
|
||||||
|
"reset_avatar_confirm": "Haluatko todella nollata profiilikuvan?",
|
||||||
|
"reset_banner_confirm": "Haluatko todella nollata profiilin taustan?"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"day": "{0} päivä",
|
"day": "{0} päivä",
|
||||||
|
@ -536,7 +561,8 @@
|
||||||
"show_new": "Näytä uudet",
|
"show_new": "Näytä uudet",
|
||||||
"up_to_date": "Ajantasalla",
|
"up_to_date": "Ajantasalla",
|
||||||
"no_more_statuses": "Ei enempää viestejä",
|
"no_more_statuses": "Ei enempää viestejä",
|
||||||
"no_statuses": "Ei viestejä"
|
"no_statuses": "Ei viestejä",
|
||||||
|
"reload": "Päivitä"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"favorites": "Tykkäykset",
|
"favorites": "Tykkäykset",
|
||||||
|
@ -551,7 +577,15 @@
|
||||||
"mute_conversation": "Mykistä keskustelu",
|
"mute_conversation": "Mykistä keskustelu",
|
||||||
"unmute_conversation": "Poista mykistys",
|
"unmute_conversation": "Poista mykistys",
|
||||||
"status_unavailable": "Viesti ei saatavissa",
|
"status_unavailable": "Viesti ei saatavissa",
|
||||||
"copy_link": "Kopioi linkki"
|
"copy_link": "Kopioi linkki",
|
||||||
|
"bookmark": "Lisää kirjanmerkkeihin",
|
||||||
|
"unbookmark": "Poista kirjanmerkeistä",
|
||||||
|
"thread_muted": "Keskustelu mykistetty",
|
||||||
|
"thread_muted_and_words": ", sisältää sanat:",
|
||||||
|
"show_full_subject": "Näytä koko otsikko",
|
||||||
|
"hide_full_subject": "Piilota koko otsikko",
|
||||||
|
"show_content": "Näytä sisältö",
|
||||||
|
"hide_content": "Piilota sisältö"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Hyväksy",
|
"approve": "Hyväksy",
|
||||||
|
@ -561,7 +595,7 @@
|
||||||
"follow": "Seuraa",
|
"follow": "Seuraa",
|
||||||
"follow_sent": "Pyyntö lähetetty!",
|
"follow_sent": "Pyyntö lähetetty!",
|
||||||
"follow_progress": "Pyydetään…",
|
"follow_progress": "Pyydetään…",
|
||||||
"follow_again": "Lähetä pyyntö uudestaan",
|
"follow_again": "Lähetä pyyntö uudestaan?",
|
||||||
"follow_unfollow": "Älä seuraa",
|
"follow_unfollow": "Älä seuraa",
|
||||||
"followees": "Seuraa",
|
"followees": "Seuraa",
|
||||||
"followers": "Seuraajat",
|
"followers": "Seuraajat",
|
||||||
|
@ -575,7 +609,7 @@
|
||||||
"statuses": "Viestit",
|
"statuses": "Viestit",
|
||||||
"hidden": "Piilotettu",
|
"hidden": "Piilotettu",
|
||||||
"media": "Media",
|
"media": "Media",
|
||||||
"block_progress": "Estetään...",
|
"block_progress": "Estetään…",
|
||||||
"admin_menu": {
|
"admin_menu": {
|
||||||
"grant_admin": "Anna Ylläpitöoikeudet",
|
"grant_admin": "Anna Ylläpitöoikeudet",
|
||||||
"force_nsfw": "Merkitse kaikki viestit NSFW:nä",
|
"force_nsfw": "Merkitse kaikki viestit NSFW:nä",
|
||||||
|
@ -601,10 +635,10 @@
|
||||||
"subscribe": "Tilaa",
|
"subscribe": "Tilaa",
|
||||||
"unsubscribe": "Poista tilaus",
|
"unsubscribe": "Poista tilaus",
|
||||||
"unblock": "Poista esto",
|
"unblock": "Poista esto",
|
||||||
"unblock_progress": "Postetaan estoa...",
|
"unblock_progress": "Poistetaan estoa…",
|
||||||
"unmute": "Poista mykistys",
|
"unmute": "Poista mykistys",
|
||||||
"unmute_progress": "Poistetaan mykistystä...",
|
"unmute_progress": "Poistetaan mykistystä…",
|
||||||
"mute_progress": "Mykistetään...",
|
"mute_progress": "Mykistetään…",
|
||||||
"hide_repeats": "Piilota toistot",
|
"hide_repeats": "Piilota toistot",
|
||||||
"show_repeats": "Näytä toistot"
|
"show_repeats": "Näytä toistot"
|
||||||
},
|
},
|
||||||
|
@ -625,7 +659,8 @@
|
||||||
"user_settings": "Käyttäjäasetukset",
|
"user_settings": "Käyttäjäasetukset",
|
||||||
"add_reaction": "Lisää Reaktio",
|
"add_reaction": "Lisää Reaktio",
|
||||||
"accept_follow_request": "Hyväksy seurauspyyntö",
|
"accept_follow_request": "Hyväksy seurauspyyntö",
|
||||||
"reject_follow_request": "Hylkää seurauspyyntö"
|
"reject_follow_request": "Hylkää seurauspyyntö",
|
||||||
|
"bookmark": "Kirjanmerkki"
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"error": {
|
"error": {
|
||||||
|
@ -674,8 +709,8 @@
|
||||||
"domain_mute_card": {
|
"domain_mute_card": {
|
||||||
"mute": "Mykistä",
|
"mute": "Mykistä",
|
||||||
"unmute": "Poista mykistys",
|
"unmute": "Poista mykistys",
|
||||||
"mute_progress": "Mykistetään...",
|
"mute_progress": "Mykistetään…",
|
||||||
"unmute_progress": "Poistetaan mykistyst..."
|
"unmute_progress": "Poistetaan mykistystä…"
|
||||||
},
|
},
|
||||||
"exporter": {
|
"exporter": {
|
||||||
"export": "Vie",
|
"export": "Vie",
|
||||||
|
@ -743,5 +778,8 @@
|
||||||
"people_talking": "{0} käyttäjää puhuvat",
|
"people_talking": "{0} käyttäjää puhuvat",
|
||||||
"person_talking": "{0} käyttäjä puhuu",
|
"person_talking": "{0} käyttäjä puhuu",
|
||||||
"no_results": "Ei tuloksia"
|
"no_results": "Ei tuloksia"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"storage_unavailable": "Pleroma ei voinut käyttää selaimen muistia. Kirjautumisesi ja paikalliset asetukset eivät tallennu ja saatat kohdata odottamattomia ongelmia. Yritä sallia evästeet."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
282
src/i18n/it.json
282
src/i18n/it.json
|
@ -34,7 +34,9 @@
|
||||||
"user_search": "Ricerca utenti",
|
"user_search": "Ricerca utenti",
|
||||||
"search": "Ricerca",
|
"search": "Ricerca",
|
||||||
"who_to_follow": "Chi seguire",
|
"who_to_follow": "Chi seguire",
|
||||||
"preferences": "Preferenze"
|
"preferences": "Preferenze",
|
||||||
|
"bookmarks": "Segnalibri",
|
||||||
|
"chats": "Conversazioni"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followed_you": "ti segue",
|
"followed_you": "ti segue",
|
||||||
|
@ -84,7 +86,7 @@
|
||||||
"change_password": "Cambia password",
|
"change_password": "Cambia password",
|
||||||
"change_password_error": "C'è stato un problema durante il cambiamento della password.",
|
"change_password_error": "C'è stato un problema durante il cambiamento della password.",
|
||||||
"changed_password": "Password cambiata correttamente!",
|
"changed_password": "Password cambiata correttamente!",
|
||||||
"collapse_subject": "Ripiega messaggi con Oggetto",
|
"collapse_subject": "Ripiega messaggi con oggetto",
|
||||||
"confirm_new_password": "Conferma la nuova password",
|
"confirm_new_password": "Conferma la nuova password",
|
||||||
"current_password": "La tua password attuale",
|
"current_password": "La tua password attuale",
|
||||||
"data_import_export_tab": "Importa o esporta dati",
|
"data_import_export_tab": "Importa o esporta dati",
|
||||||
|
@ -255,7 +257,13 @@
|
||||||
"top_bar": "Barra superiore",
|
"top_bar": "Barra superiore",
|
||||||
"panel_header": "Titolo pannello",
|
"panel_header": "Titolo pannello",
|
||||||
"badge_notification": "Notifica",
|
"badge_notification": "Notifica",
|
||||||
"popover": "Suggerimenti, menù, sbalzi"
|
"popover": "Suggerimenti, menù, sbalzi",
|
||||||
|
"toggled": "Scambiato",
|
||||||
|
"chat": {
|
||||||
|
"border": "Bordo",
|
||||||
|
"outgoing": "Inviati",
|
||||||
|
"incoming": "Ricevuti"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"common_colors": {
|
"common_colors": {
|
||||||
"rgbo": "Icone, accenti, medaglie",
|
"rgbo": "Icone, accenti, medaglie",
|
||||||
|
@ -270,10 +278,59 @@
|
||||||
"shadow_id": "Ombra numero {value}",
|
"shadow_id": "Ombra numero {value}",
|
||||||
"override": "Sostituisci",
|
"override": "Sostituisci",
|
||||||
"component": "Componente",
|
"component": "Componente",
|
||||||
"_tab_label": "Luci ed ombre"
|
"_tab_label": "Luci ed ombre",
|
||||||
|
"components": {
|
||||||
|
"avatarStatus": "Icona utente (vista messaggio)",
|
||||||
|
"avatar": "Icona utente (vista profilo)",
|
||||||
|
"topBar": "Barra superiore",
|
||||||
|
"panelHeader": "Intestazione pannello",
|
||||||
|
"panel": "Pannello",
|
||||||
|
"input": "Campo d'immissione",
|
||||||
|
"buttonPressedHover": "Pulsante (puntato e premuto)",
|
||||||
|
"buttonPressed": "Pulsante (premuto)",
|
||||||
|
"buttonHover": "Pulsante (puntato)",
|
||||||
|
"button": "Pulsante",
|
||||||
|
"popup": "Sbalzi e suggerimenti"
|
||||||
|
},
|
||||||
|
"filter_hint": {
|
||||||
|
"inset_classic": "Le ombre incluse usano {0}",
|
||||||
|
"spread_zero": "Lo spandimento maggiore di zero si azzera sulle ombre",
|
||||||
|
"avatar_inset": "Tieni presente che combinare ombre (sia incluse che non) sulle icone utente potrebbe dare risultati strani con quelle trasparenti.",
|
||||||
|
"drop_shadow_syntax": "{0} non supporta il parametro {1} né la keyword {2}.",
|
||||||
|
"always_drop_shadow": "Attenzione: quest'ombra usa sempre {0} se il tuo browser lo supporta."
|
||||||
|
},
|
||||||
|
"hintV3": "Per le ombre puoi anche usare la sintassi {0} per sfruttare il secondo colore."
|
||||||
},
|
},
|
||||||
"radii": {
|
"radii": {
|
||||||
"_tab_label": "Raggio"
|
"_tab_label": "Raggio"
|
||||||
|
},
|
||||||
|
"fonts": {
|
||||||
|
"_tab_label": "Font",
|
||||||
|
"custom": "Personalizzato",
|
||||||
|
"weight": "Peso (grassettatura)",
|
||||||
|
"size": "Dimensione (in pixel)",
|
||||||
|
"family": "Nome font",
|
||||||
|
"components": {
|
||||||
|
"postCode": "Font a spaziatura fissa incluso in un messaggio",
|
||||||
|
"post": "Testo del messaggio",
|
||||||
|
"input": "Campi d'immissione",
|
||||||
|
"interface": "Interfaccia"
|
||||||
|
},
|
||||||
|
"help": "Seleziona il font da usare per gli elementi dell'interfaccia. Se scegli \"personalizzato\" devi inserire il suo nome di sistema."
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"link": "un bel collegamentino",
|
||||||
|
"checkbox": "Ho dato uno sguardo a termini e condizioni",
|
||||||
|
"header_faint": "Tutto bene",
|
||||||
|
"fine_print": "Leggi il nostro {0} per imparare un bel niente!",
|
||||||
|
"faint_link": "utilissimo manuale",
|
||||||
|
"input": "Sono appena atterrato a Fiumicino.",
|
||||||
|
"mono": "contenuto",
|
||||||
|
"text": "Altro {0} e {1}",
|
||||||
|
"content": "Contenuto",
|
||||||
|
"button": "Pulsante",
|
||||||
|
"error": "Errore d'esempio",
|
||||||
|
"header": "Anteprima"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enable_web_push_notifications": "Abilita notifiche web push",
|
"enable_web_push_notifications": "Abilita notifiche web push",
|
||||||
|
@ -335,7 +392,26 @@
|
||||||
"emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze",
|
"emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze",
|
||||||
"pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore",
|
"pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore",
|
||||||
"notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.",
|
"notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.",
|
||||||
"mutes_and_blocks": "Zittiti e bloccati"
|
"mutes_and_blocks": "Zittiti e bloccati",
|
||||||
|
"profile_fields": {
|
||||||
|
"value": "Contenuto",
|
||||||
|
"name": "Etichetta",
|
||||||
|
"add_field": "Aggiungi campo",
|
||||||
|
"label": "Metadati profilo"
|
||||||
|
},
|
||||||
|
"bot": "Questo profilo è di un robot",
|
||||||
|
"version": {
|
||||||
|
"frontend_version": "Versione interfaccia",
|
||||||
|
"backend_version": "Versione backend",
|
||||||
|
"title": "Versione"
|
||||||
|
},
|
||||||
|
"reset_avatar": "Azzera icona",
|
||||||
|
"reset_profile_background": "Azzera sfondo profilo",
|
||||||
|
"reset_profile_banner": "Azzera stendardo profilo",
|
||||||
|
"reset_avatar_confirm": "Vuoi veramente azzerare l'icona?",
|
||||||
|
"reset_banner_confirm": "Vuoi veramente azzerare lo stendardo?",
|
||||||
|
"reset_background_confirm": "Vuoi veramente azzerare lo sfondo?",
|
||||||
|
"chatMessageRadius": "Messaggi istantanei"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"error_fetching": "Errore nell'aggiornamento",
|
"error_fetching": "Errore nell'aggiornamento",
|
||||||
|
@ -345,7 +421,10 @@
|
||||||
"collapse": "Riduci",
|
"collapse": "Riduci",
|
||||||
"conversation": "Conversazione",
|
"conversation": "Conversazione",
|
||||||
"no_retweet_hint": "Il messaggio è diretto o solo per seguaci e non può essere condiviso",
|
"no_retweet_hint": "Il messaggio è diretto o solo per seguaci e non può essere condiviso",
|
||||||
"repeated": "condiviso"
|
"repeated": "condiviso",
|
||||||
|
"no_statuses": "Nessun messaggio",
|
||||||
|
"no_more_statuses": "Fine dei messaggi",
|
||||||
|
"reload": "Ricarica"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"follow": "Segui",
|
"follow": "Segui",
|
||||||
|
@ -361,7 +440,47 @@
|
||||||
"block": "Blocca",
|
"block": "Blocca",
|
||||||
"blocked": "Bloccato!",
|
"blocked": "Bloccato!",
|
||||||
"deny": "Nega",
|
"deny": "Nega",
|
||||||
"remote_follow": "Segui da remoto"
|
"remote_follow": "Segui da remoto",
|
||||||
|
"admin_menu": {
|
||||||
|
"delete_user_confirmation": "Ne sei completamente sicuro? Quest'azione non può essere annullata.",
|
||||||
|
"delete_user": "Elimina utente",
|
||||||
|
"quarantine": "I messaggi non arriveranno alle altre stanze",
|
||||||
|
"disable_any_subscription": "Rendi utente non seguibile",
|
||||||
|
"disable_remote_subscription": "Blocca i tentativi di seguirlo da altre stanze",
|
||||||
|
"sandbox": "Rendi tutti i messaggi solo per seguaci",
|
||||||
|
"force_unlisted": "Rendi tutti i messaggi invisibili",
|
||||||
|
"strip_media": "Rimuovi ogni allegato ai messaggi",
|
||||||
|
"force_nsfw": "Oscura tutti i messaggi",
|
||||||
|
"delete_account": "Elimina profilo",
|
||||||
|
"deactivate_account": "Disattiva profilo",
|
||||||
|
"activate_account": "Attiva profilo",
|
||||||
|
"revoke_moderator": "Divesti Moderatore",
|
||||||
|
"grant_moderator": "Crea Moderatore",
|
||||||
|
"revoke_admin": "Divesti Amministratore",
|
||||||
|
"grant_admin": "Crea Amministratore",
|
||||||
|
"moderation": "Moderazione"
|
||||||
|
},
|
||||||
|
"show_repeats": "Mostra condivisioni",
|
||||||
|
"hide_repeats": "Nascondi condivisioni",
|
||||||
|
"mute_progress": "Zittisco…",
|
||||||
|
"unmute_progress": "Riabilito…",
|
||||||
|
"unmute": "Riabilita",
|
||||||
|
"block_progress": "Blocco…",
|
||||||
|
"unblock_progress": "Sblocco…",
|
||||||
|
"unblock": "Sblocca",
|
||||||
|
"unsubscribe": "Disdici",
|
||||||
|
"subscribe": "Abbònati",
|
||||||
|
"report": "Segnala",
|
||||||
|
"mention": "Menzioni",
|
||||||
|
"media": "Media",
|
||||||
|
"its_you": "Sei tu!",
|
||||||
|
"hidden": "Nascosto",
|
||||||
|
"follow_unfollow": "Disconosci",
|
||||||
|
"follow_again": "Reinvio richiesta?",
|
||||||
|
"follow_progress": "Richiedo…",
|
||||||
|
"follow_sent": "Richiesta inviata!",
|
||||||
|
"favorites": "Preferiti",
|
||||||
|
"message": "Contatta"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"title": "Chat"
|
"title": "Chat"
|
||||||
|
@ -373,7 +492,8 @@
|
||||||
"scope_options": "Opzioni visibilità",
|
"scope_options": "Opzioni visibilità",
|
||||||
"text_limit": "Lunghezza massima",
|
"text_limit": "Lunghezza massima",
|
||||||
"title": "Caratteristiche",
|
"title": "Caratteristiche",
|
||||||
"who_to_follow": "Chi seguire"
|
"who_to_follow": "Chi seguire",
|
||||||
|
"pleroma_chat_messages": "Chiacchiere"
|
||||||
},
|
},
|
||||||
"finder": {
|
"finder": {
|
||||||
"error_fetching_user": "Errore nel recupero dell'utente",
|
"error_fetching_user": "Errore nel recupero dell'utente",
|
||||||
|
@ -424,7 +544,12 @@
|
||||||
},
|
},
|
||||||
"direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati all'inizio.",
|
"direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati all'inizio.",
|
||||||
"direct_warning_to_all": "Questo messaggio sarà visibile a tutti i menzionati.",
|
"direct_warning_to_all": "Questo messaggio sarà visibile a tutti i menzionati.",
|
||||||
"new_status": "Nuovo messaggio"
|
"new_status": "Nuovo messaggio",
|
||||||
|
"empty_status_error": "Non puoi pubblicare messaggi vuoti senza allegati",
|
||||||
|
"preview_empty": "Vuoto",
|
||||||
|
"preview": "Anteprima",
|
||||||
|
"media_description_error": "Allegati non caricati, riprova",
|
||||||
|
"media_description": "Descrizione allegati"
|
||||||
},
|
},
|
||||||
"registration": {
|
"registration": {
|
||||||
"bio": "Introduzione",
|
"bio": "Introduzione",
|
||||||
|
@ -448,7 +573,9 @@
|
||||||
"captcha": "CAPTCHA"
|
"captcha": "CAPTCHA"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"timeline_title": "Sequenza dell'Utente"
|
"timeline_title": "Sequenza dell'Utente",
|
||||||
|
"profile_loading_error": "Spiacente, c'è stato un errore nel caricamento del profilo.",
|
||||||
|
"profile_does_not_exist": "Spiacente, questo profilo non esiste."
|
||||||
},
|
},
|
||||||
"who_to_follow": {
|
"who_to_follow": {
|
||||||
"more": "Altro",
|
"more": "Altro",
|
||||||
|
@ -547,5 +674,140 @@
|
||||||
"error": "Non trovato.",
|
"error": "Non trovato.",
|
||||||
"searching_for": "Cerco",
|
"searching_for": "Cerco",
|
||||||
"remote_user_resolver": "Cerca utenti remoti"
|
"remote_user_resolver": "Cerca utenti remoti"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"storage_unavailable": "Pleroma non ha potuto accedere ai dati del tuo browser. Le tue credenziali o le tue impostazioni locali non potranno essere salvate e potresti incontrare strani errori. Prova ad abilitare i cookie."
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"pinned": "Intestato",
|
||||||
|
"unpin": "De-intesta",
|
||||||
|
"pin": "Intesta al profilo",
|
||||||
|
"delete": "Elimina messaggio",
|
||||||
|
"repeats": "Condivisi",
|
||||||
|
"favorites": "Preferiti",
|
||||||
|
"hide_content": "Nascondi contenuti",
|
||||||
|
"show_content": "Mostra contenuti",
|
||||||
|
"hide_full_subject": "Nascondi intero oggetto",
|
||||||
|
"show_full_subject": "Mostra intero oggetto",
|
||||||
|
"thread_muted_and_words": ", contiene:",
|
||||||
|
"thread_muted": "Discussione zittita",
|
||||||
|
"copy_link": "Copia collegamento",
|
||||||
|
"status_unavailable": "Messaggio non disponibile",
|
||||||
|
"unmute_conversation": "Riabilita conversazione",
|
||||||
|
"mute_conversation": "Zittisci conversazione",
|
||||||
|
"replies_list": "Risposte:",
|
||||||
|
"reply_to": "Rispondi a",
|
||||||
|
"delete_confirm": "Vuoi veramente eliminare questo messaggio?",
|
||||||
|
"unbookmark": "Rimuovi segnalibro",
|
||||||
|
"bookmark": "Aggiungi segnalibro"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"years_short": "{0}a",
|
||||||
|
"year_short": "{0}a",
|
||||||
|
"years": "{0} anni",
|
||||||
|
"year": "{0} anno",
|
||||||
|
"weeks_short": "{0}set",
|
||||||
|
"week_short": "{0}set",
|
||||||
|
"seconds_short": "{0}sec",
|
||||||
|
"second_short": "{0}sec",
|
||||||
|
"weeks": "{0} settimane",
|
||||||
|
"week": "{0} settimana",
|
||||||
|
"seconds": "{0} secondi",
|
||||||
|
"second": "{0} secondo",
|
||||||
|
"now_short": "ora",
|
||||||
|
"now": "adesso",
|
||||||
|
"months_short": "{0}me",
|
||||||
|
"month_short": "{0}me",
|
||||||
|
"months": "{0} mesi",
|
||||||
|
"month": "{0} mese",
|
||||||
|
"minutes_short": "{0}min",
|
||||||
|
"minute_short": "{0}min",
|
||||||
|
"minutes": "{0} minuti",
|
||||||
|
"minute": "{0} minuto",
|
||||||
|
"in_past": "{0} fa",
|
||||||
|
"in_future": "fra {0}",
|
||||||
|
"hours_short": "{0}h",
|
||||||
|
"days_short": "{0}g",
|
||||||
|
"hour_short": "{0}h",
|
||||||
|
"hours": "{0} ore",
|
||||||
|
"hour": "{0} ora",
|
||||||
|
"day_short": "{0}g",
|
||||||
|
"days": "{0} giorni",
|
||||||
|
"day": "{0} giorno"
|
||||||
|
},
|
||||||
|
"user_reporting": {
|
||||||
|
"title": "Segnalo {0}",
|
||||||
|
"additional_comments": "Osservazioni accessorie",
|
||||||
|
"generic_error": "C'è stato un errore nell'elaborazione della tua richiesta.",
|
||||||
|
"submit": "Invia",
|
||||||
|
"forward_to": "Inoltra a {0}",
|
||||||
|
"forward_description": "Il profilo appartiene ad un'altra stanza. Inviare la segnalazione anche a quella?",
|
||||||
|
"add_comment_description": "La segnalazione sarà inviata ai moderatori della tua stanza. Puoi motivarla qui sotto:"
|
||||||
|
},
|
||||||
|
"password_reset": {
|
||||||
|
"password_reset_required_but_mailer_is_disabled": "Devi reimpostare la tua password, ma non puoi farlo. Contatta il tuo amministratore.",
|
||||||
|
"password_reset_required": "Devi reimpostare la tua password per poter continuare.",
|
||||||
|
"password_reset_disabled": "Non puoi azzerare la tua password. Contatta il tuo amministratore.",
|
||||||
|
"too_many_requests": "Hai raggiunto il numero massimo di tentativi, riprova più tardi.",
|
||||||
|
"not_found": "Non ho trovato questa email o nome utente.",
|
||||||
|
"return_home": "Torna alla pagina principale",
|
||||||
|
"check_email": "Controlla la tua posta elettronica.",
|
||||||
|
"placeholder": "La tua email o nome utente",
|
||||||
|
"instruction": "Inserisci il tuo indirizzo email o il tuo nome utente. Ti invieremo un collegamento per reimpostare la tua password.",
|
||||||
|
"password_reset": "Azzera password",
|
||||||
|
"forgot_password": "Password dimenticata?"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"no_results": "Nessun risultato",
|
||||||
|
"people_talking": "{count} partecipanti",
|
||||||
|
"person_talking": "{count} partecipante",
|
||||||
|
"hashtags": "Etichette",
|
||||||
|
"people": "Utenti"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"file_size_units": {
|
||||||
|
"TiB": "TiB",
|
||||||
|
"GiB": "GiB",
|
||||||
|
"MiB": "MiB",
|
||||||
|
"KiB": "KiB",
|
||||||
|
"B": "B"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"default": "Riprova in seguito",
|
||||||
|
"file_too_big": "File troppo pesante [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||||
|
"base": "Caricamento fallito."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tool_tip": {
|
||||||
|
"bookmark": "Aggiungi segnalibro",
|
||||||
|
"reject_follow_request": "Rifiuta seguace",
|
||||||
|
"accept_follow_request": "Accetta seguace",
|
||||||
|
"user_settings": "Impostazioni utente",
|
||||||
|
"add_reaction": "Reagisci",
|
||||||
|
"favorite": "Gradisci",
|
||||||
|
"reply": "Rispondi",
|
||||||
|
"repeat": "Ripeti",
|
||||||
|
"media_upload": "Carica allegati"
|
||||||
|
},
|
||||||
|
"display_date": {
|
||||||
|
"today": "Oggi"
|
||||||
|
},
|
||||||
|
"file_type": {
|
||||||
|
"file": "File",
|
||||||
|
"image": "Immagine",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio"
|
||||||
|
},
|
||||||
|
"chats": {
|
||||||
|
"empty_chat_list_placeholder": "Non hai conversazioni. Contatta qualcuno!",
|
||||||
|
"error_sending_message": "Errore. Il messaggio non è stato inviato.",
|
||||||
|
"error_loading_chat": "Errore. La conversazione non è stata caricata.",
|
||||||
|
"delete_confirm": "Vuoi veramente eliminare questo messaggio?",
|
||||||
|
"more": "Altro",
|
||||||
|
"empty_message_error": "Non puoi inviare messaggi vuoti",
|
||||||
|
"new": "Nuova conversazione",
|
||||||
|
"chats": "Conversazioni",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"message_user": "Contatta {nickname}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,12 @@
|
||||||
"enable": "Inschakelen",
|
"enable": "Inschakelen",
|
||||||
"confirm": "Bevestigen",
|
"confirm": "Bevestigen",
|
||||||
"verify": "Verifiëren",
|
"verify": "Verifiëren",
|
||||||
"generic_error": "Er is een fout opgetreden"
|
"generic_error": "Er is een fout opgetreden",
|
||||||
|
"peek": "Spiek",
|
||||||
|
"close": "Sluiten",
|
||||||
|
"retry": "Opnieuw proberen",
|
||||||
|
"error_retry": "Probeer het opnieuw",
|
||||||
|
"loading": "Laden…"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "Log in",
|
"login": "Log in",
|
||||||
|
@ -90,7 +95,7 @@
|
||||||
"text/bbcode": "BBCode"
|
"text/bbcode": "BBCode"
|
||||||
},
|
},
|
||||||
"content_warning": "Onderwerp (optioneel)",
|
"content_warning": "Onderwerp (optioneel)",
|
||||||
"default": "Zojuist geland in L.A.",
|
"default": "Tijd voor anime!",
|
||||||
"direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.",
|
"direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.",
|
||||||
"posting": "Plaatsen",
|
"posting": "Plaatsen",
|
||||||
"scope": {
|
"scope": {
|
||||||
|
@ -377,7 +382,7 @@
|
||||||
"button": "Knop",
|
"button": "Knop",
|
||||||
"text": "Nog een boel andere {0} en {1}",
|
"text": "Nog een boel andere {0} en {1}",
|
||||||
"mono": "inhoud",
|
"mono": "inhoud",
|
||||||
"input": "Zojuist geland in L.A.",
|
"input": "Tijd voor anime!",
|
||||||
"faint_link": "handige gebruikershandleiding",
|
"faint_link": "handige gebruikershandleiding",
|
||||||
"fine_print": "Lees onze {0} om niets nuttig te leren!",
|
"fine_print": "Lees onze {0} om niets nuttig te leren!",
|
||||||
"header_faint": "Alles komt goed",
|
"header_faint": "Alles komt goed",
|
||||||
|
@ -451,7 +456,7 @@
|
||||||
"user_mutes": "Gebruikers",
|
"user_mutes": "Gebruikers",
|
||||||
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
||||||
"useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)",
|
"useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)",
|
||||||
"type_domains_to_mute": "Voer domeinen in om te negeren",
|
"type_domains_to_mute": "Zoek domeinen om te negeren",
|
||||||
"upload_a_photo": "Upload een foto",
|
"upload_a_photo": "Upload een foto",
|
||||||
"fun": "Plezier",
|
"fun": "Plezier",
|
||||||
"greentext": "Meme pijlen",
|
"greentext": "Meme pijlen",
|
||||||
|
@ -470,7 +475,15 @@
|
||||||
"frontend_version": "Frontend Versie",
|
"frontend_version": "Frontend Versie",
|
||||||
"backend_version": "Backend Versie",
|
"backend_version": "Backend Versie",
|
||||||
"title": "Versie"
|
"title": "Versie"
|
||||||
}
|
},
|
||||||
|
"mutes_and_blocks": "Negeringen en Blokkades",
|
||||||
|
"profile_fields": {
|
||||||
|
"value": "Inhoud",
|
||||||
|
"name": "Label",
|
||||||
|
"add_field": "Veld Toevoegen",
|
||||||
|
"label": "Profiel metadata"
|
||||||
|
},
|
||||||
|
"bot": "Dit is een bot account"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"collapse": "Inklappen",
|
"collapse": "Inklappen",
|
||||||
|
@ -708,7 +721,9 @@
|
||||||
"unpin": "Van profiel losmaken",
|
"unpin": "Van profiel losmaken",
|
||||||
"delete": "Status verwijderen",
|
"delete": "Status verwijderen",
|
||||||
"repeats": "Herhalingen",
|
"repeats": "Herhalingen",
|
||||||
"favorites": "Favorieten"
|
"favorites": "Favorieten",
|
||||||
|
"thread_muted_and_words": ", heeft woorden:",
|
||||||
|
"thread_muted": "Thread genegeerd"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"years_short": "{0}j",
|
"years_short": "{0}j",
|
||||||
|
|
|
@ -45,7 +45,8 @@
|
||||||
"timeline": "Лента",
|
"timeline": "Лента",
|
||||||
"twkn": "Федеративная лента",
|
"twkn": "Федеративная лента",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"friend_requests": "Запросы на чтение"
|
"friend_requests": "Запросы на чтение",
|
||||||
|
"bookmarks": "Закладки"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Неизвестный статус, ищем...",
|
"broken_favorite": "Неизвестный статус, ищем...",
|
||||||
|
@ -366,6 +367,10 @@
|
||||||
"show_new": "Показать новые",
|
"show_new": "Показать новые",
|
||||||
"up_to_date": "Обновлено"
|
"up_to_date": "Обновлено"
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
"bookmark": "В закладки",
|
||||||
|
"unbookmark": "Удалить из закладок"
|
||||||
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"block": "Заблокировать",
|
"block": "Заблокировать",
|
||||||
"blocked": "Заблокирован",
|
"blocked": "Заблокирован",
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
"administration": "管理员"
|
"administration": "管理员"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "未知的状态,正在搜索中...",
|
"broken_favorite": "未知的状态,正在搜索中…",
|
||||||
"favorited_you": "收藏了你的状态",
|
"favorited_you": "收藏了你的状态",
|
||||||
"followed_you": "关注了你",
|
"followed_you": "关注了你",
|
||||||
"load_older": "加载更早的通知",
|
"load_older": "加载更早的通知",
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
"generate_new_recovery_codes": "生成新的恢复码",
|
"generate_new_recovery_codes": "生成新的恢复码",
|
||||||
"warning_of_generate_new_codes": "当你生成新的恢复码时,你的旧恢复码就失效了。",
|
"warning_of_generate_new_codes": "当你生成新的恢复码时,你的旧恢复码就失效了。",
|
||||||
"recovery_codes": "恢复码。",
|
"recovery_codes": "恢复码。",
|
||||||
"waiting_a_recovery_codes": "正在接收备份码……",
|
"waiting_a_recovery_codes": "正在接收备份码…",
|
||||||
"recovery_codes_warning": "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。",
|
"recovery_codes_warning": "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。",
|
||||||
"authentication_methods": "身份验证方法",
|
"authentication_methods": "身份验证方法",
|
||||||
"scan": {
|
"scan": {
|
||||||
|
@ -564,11 +564,11 @@
|
||||||
"subscribe": "订阅",
|
"subscribe": "订阅",
|
||||||
"unsubscribe": "退订",
|
"unsubscribe": "退订",
|
||||||
"unblock": "取消拉黑",
|
"unblock": "取消拉黑",
|
||||||
"unblock_progress": "取消拉黑中...",
|
"unblock_progress": "取消拉黑中…",
|
||||||
"block_progress": "拉黑中...",
|
"block_progress": "拉黑中…",
|
||||||
"unmute": "取消隐藏",
|
"unmute": "取消隐藏",
|
||||||
"unmute_progress": "取消隐藏中...",
|
"unmute_progress": "取消隐藏中…",
|
||||||
"mute_progress": "隐藏中...",
|
"mute_progress": "隐藏中…",
|
||||||
"admin_menu": {
|
"admin_menu": {
|
||||||
"moderation": "权限",
|
"moderation": "权限",
|
||||||
"grant_admin": "赋予管理权限",
|
"grant_admin": "赋予管理权限",
|
||||||
|
@ -690,9 +690,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain_mute_card": {
|
"domain_mute_card": {
|
||||||
"unmute_progress": "正在取消隐藏……",
|
"unmute_progress": "正在取消隐藏…",
|
||||||
"unmute": "取消隐藏",
|
"unmute": "取消隐藏",
|
||||||
"mute_progress": "隐藏中……",
|
"mute_progress": "隐藏中…",
|
||||||
"mute": "隐藏"
|
"mute": "隐藏"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
src/main.js
20
src/main.js
|
@ -19,6 +19,7 @@ import oauthTokensModule from './modules/oauth_tokens.js'
|
||||||
import reportsModule from './modules/reports.js'
|
import reportsModule from './modules/reports.js'
|
||||||
import pollsModule from './modules/polls.js'
|
import pollsModule from './modules/polls.js'
|
||||||
import postStatusModule from './modules/postStatus.js'
|
import postStatusModule from './modules/postStatus.js'
|
||||||
|
import chatsModule from './modules/chats.js'
|
||||||
|
|
||||||
import VueI18n from 'vue-i18n'
|
import VueI18n from 'vue-i18n'
|
||||||
|
|
||||||
|
@ -62,7 +63,15 @@ const persistedStateOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const persistedState = await createPersistedState(persistedStateOptions)
|
let storageError = false
|
||||||
|
const plugins = [pushNotifications]
|
||||||
|
try {
|
||||||
|
const persistedState = await createPersistedState(persistedStateOptions)
|
||||||
|
plugins.push(persistedState)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
storageError = true
|
||||||
|
}
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
i18n: {
|
i18n: {
|
||||||
|
@ -83,13 +92,16 @@ const persistedStateOptions = {
|
||||||
oauthTokens: oauthTokensModule,
|
oauthTokens: oauthTokensModule,
|
||||||
reports: reportsModule,
|
reports: reportsModule,
|
||||||
polls: pollsModule,
|
polls: pollsModule,
|
||||||
postStatus: postStatusModule
|
postStatus: postStatusModule,
|
||||||
|
chats: chatsModule
|
||||||
},
|
},
|
||||||
plugins: [persistedState, pushNotifications],
|
plugins,
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
// strict: process.env.NODE_ENV !== 'production'
|
// strict: process.env.NODE_ENV !== 'production'
|
||||||
})
|
})
|
||||||
|
if (storageError) {
|
||||||
|
store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
|
||||||
|
}
|
||||||
afterStoreSetup({ store, i18n })
|
afterStoreSetup({ store, i18n })
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
|
import { WSConnectionStatus } from '../services/api/api.service.js'
|
||||||
import { Socket } from 'phoenix'
|
import { Socket } from 'phoenix'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
|
@ -7,6 +8,7 @@ const api = {
|
||||||
fetchers: {},
|
fetchers: {},
|
||||||
socket: null,
|
socket: null,
|
||||||
mastoUserSocket: null,
|
mastoUserSocket: null,
|
||||||
|
mastoUserSocketStatus: null,
|
||||||
followRequests: []
|
followRequests: []
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -28,6 +30,9 @@ const api = {
|
||||||
},
|
},
|
||||||
setFollowRequests (state, value) {
|
setFollowRequests (state, value) {
|
||||||
state.followRequests = value
|
state.followRequests = value
|
||||||
|
},
|
||||||
|
setMastoUserSocketStatus (state, value) {
|
||||||
|
state.mastoUserSocketStatus = value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -47,7 +52,7 @@ const api = {
|
||||||
startMastoUserSocket (store) {
|
startMastoUserSocket (store) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const { state, dispatch, rootState } = store
|
const { state, commit, dispatch, rootState } = store
|
||||||
const timelineData = rootState.statuses.timelines.friends
|
const timelineData = rootState.statuses.timelines.friends
|
||||||
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
|
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
|
||||||
state.mastoUserSocket.addEventListener(
|
state.mastoUserSocket.addEventListener(
|
||||||
|
@ -66,11 +71,22 @@ const api = {
|
||||||
showImmediately: timelineData.visibleStatuses.length === 0,
|
showImmediately: timelineData.visibleStatuses.length === 0,
|
||||||
timeline: 'friends'
|
timeline: 'friends'
|
||||||
})
|
})
|
||||||
|
} else if (message.event === 'pleroma:chat_update') {
|
||||||
|
dispatch('addChatMessages', {
|
||||||
|
chatId: message.chatUpdate.id,
|
||||||
|
messages: [message.chatUpdate.lastMessage]
|
||||||
|
})
|
||||||
|
dispatch('updateChat', { chat: message.chatUpdate })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
state.mastoUserSocket.addEventListener('open', () => {
|
||||||
|
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
|
||||||
|
})
|
||||||
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
||||||
console.error('Error in MastoAPI websocket:', error)
|
console.error('Error in MastoAPI websocket:', error)
|
||||||
|
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
|
||||||
|
dispatch('clearOpenedChats')
|
||||||
})
|
})
|
||||||
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
|
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
|
||||||
const ignoreCodes = new Set([
|
const ignoreCodes = new Set([
|
||||||
|
@ -84,8 +100,11 @@ const api = {
|
||||||
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
|
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
|
dispatch('startFetchingChats')
|
||||||
dispatch('restartMastoUserSocket')
|
dispatch('restartMastoUserSocket')
|
||||||
}
|
}
|
||||||
|
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
|
||||||
|
dispatch('clearOpenedChats')
|
||||||
})
|
})
|
||||||
resolve()
|
resolve()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -99,12 +118,13 @@ const api = {
|
||||||
return dispatch('startMastoUserSocket').then(() => {
|
return dispatch('startMastoUserSocket').then(() => {
|
||||||
dispatch('stopFetchingTimeline', { timeline: 'friends' })
|
dispatch('stopFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('stopFetchingNotifications')
|
dispatch('stopFetchingNotifications')
|
||||||
|
dispatch('stopFetchingChats')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
stopMastoUserSocket ({ state, dispatch }) {
|
stopMastoUserSocket ({ state, dispatch }) {
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
console.log(state.mastoUserSocket)
|
dispatch('startFetchingChats')
|
||||||
state.mastoUserSocket.close()
|
state.mastoUserSocket.close()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -138,9 +158,6 @@ const api = {
|
||||||
if (!fetcher) return
|
if (!fetcher) return
|
||||||
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
||||||
},
|
},
|
||||||
fetchAndUpdateNotifications (store) {
|
|
||||||
store.state.backendInteractor.fetchAndUpdateNotifications({ store })
|
|
||||||
},
|
|
||||||
|
|
||||||
// Follow requests
|
// Follow requests
|
||||||
startFetchingFollowRequests (store) {
|
startFetchingFollowRequests (store) {
|
||||||
|
|
225
src/modules/chats.js
Normal file
225
src/modules/chats.js
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { find, omitBy, orderBy, sumBy } from 'lodash'
|
||||||
|
import chatService from '../services/chat_service/chat_service.js'
|
||||||
|
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const emptyChatList = () => ({
|
||||||
|
data: [],
|
||||||
|
idStore: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
chatList: emptyChatList(),
|
||||||
|
chatListFetcher: null,
|
||||||
|
openedChats: {},
|
||||||
|
openedChatMessageServices: {},
|
||||||
|
fetcher: undefined,
|
||||||
|
currentChatId: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChatById = (state, id) => {
|
||||||
|
return find(state.chatList.data, { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedChatList = (state) => {
|
||||||
|
return orderBy(state.chatList.data, ['updated_at'], ['desc'])
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadChatCount = (state) => {
|
||||||
|
return sumBy(state.chatList.data, 'unread')
|
||||||
|
}
|
||||||
|
|
||||||
|
const chats = {
|
||||||
|
state: { ...defaultState },
|
||||||
|
getters: {
|
||||||
|
currentChat: state => state.openedChats[state.currentChatId],
|
||||||
|
currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
|
||||||
|
findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
|
||||||
|
sortedChatList,
|
||||||
|
unreadChatCount
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Chat list
|
||||||
|
startFetchingChats ({ dispatch, commit }) {
|
||||||
|
const fetcher = () => {
|
||||||
|
dispatch('fetchChats', { latest: true })
|
||||||
|
}
|
||||||
|
fetcher()
|
||||||
|
commit('setChatListFetcher', {
|
||||||
|
fetcher: () => setInterval(() => { fetcher() }, 5000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stopFetchingChats ({ commit }) {
|
||||||
|
commit('setChatListFetcher', { fetcher: undefined })
|
||||||
|
},
|
||||||
|
fetchChats ({ dispatch, rootState, commit }, params = {}) {
|
||||||
|
return rootState.api.backendInteractor.chats()
|
||||||
|
.then(({ chats }) => {
|
||||||
|
dispatch('addNewChats', { chats })
|
||||||
|
return chats
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
|
||||||
|
commit('addNewChats', { dispatch, chats, rootGetters })
|
||||||
|
},
|
||||||
|
updateChat ({ commit }, { chat }) {
|
||||||
|
commit('updateChat', { chat })
|
||||||
|
},
|
||||||
|
|
||||||
|
// Opened Chats
|
||||||
|
startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
|
||||||
|
dispatch('setCurrentChatFetcher', { fetcher })
|
||||||
|
},
|
||||||
|
setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
|
||||||
|
commit('setCurrentChatFetcher', { fetcher })
|
||||||
|
},
|
||||||
|
addOpenedChat ({ rootState, commit, dispatch }, { chat }) {
|
||||||
|
commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
|
||||||
|
dispatch('addNewUsers', [chat.account])
|
||||||
|
},
|
||||||
|
addChatMessages ({ commit }, value) {
|
||||||
|
commit('addChatMessages', { commit, ...value })
|
||||||
|
},
|
||||||
|
resetChatNewMessageCount ({ commit }, value) {
|
||||||
|
commit('resetChatNewMessageCount', value)
|
||||||
|
},
|
||||||
|
clearCurrentChat ({ rootState, commit, dispatch }, value) {
|
||||||
|
commit('setCurrentChatId', { chatId: undefined })
|
||||||
|
commit('setCurrentChatFetcher', { fetcher: undefined })
|
||||||
|
},
|
||||||
|
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
||||||
|
dispatch('resetChatNewMessageCount')
|
||||||
|
commit('readChat', { id })
|
||||||
|
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
||||||
|
},
|
||||||
|
deleteChatMessage ({ rootState, commit }, value) {
|
||||||
|
rootState.api.backendInteractor.deleteChatMessage(value)
|
||||||
|
commit('deleteChatMessage', { commit, ...value })
|
||||||
|
},
|
||||||
|
resetChats ({ commit, dispatch }) {
|
||||||
|
dispatch('clearCurrentChat')
|
||||||
|
commit('resetChats', { commit })
|
||||||
|
},
|
||||||
|
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
||||||
|
commit('clearOpenedChats', { commit })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setChatListFetcher (state, { commit, fetcher }) {
|
||||||
|
const prevFetcher = state.chatListFetcher
|
||||||
|
if (prevFetcher) {
|
||||||
|
clearInterval(prevFetcher)
|
||||||
|
}
|
||||||
|
state.chatListFetcher = fetcher && fetcher()
|
||||||
|
},
|
||||||
|
setCurrentChatFetcher (state, { fetcher }) {
|
||||||
|
const prevFetcher = state.fetcher
|
||||||
|
if (prevFetcher) {
|
||||||
|
clearInterval(prevFetcher)
|
||||||
|
}
|
||||||
|
state.fetcher = fetcher && fetcher()
|
||||||
|
},
|
||||||
|
addOpenedChat (state, { _dispatch, chat }) {
|
||||||
|
state.currentChatId = chat.id
|
||||||
|
Vue.set(state.openedChats, chat.id, chat)
|
||||||
|
|
||||||
|
if (!state.openedChatMessageServices[chat.id]) {
|
||||||
|
Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentChatId (state, { chatId }) {
|
||||||
|
state.currentChatId = chatId
|
||||||
|
},
|
||||||
|
addNewChats (state, { _dispatch, chats, _rootGetters }) {
|
||||||
|
chats.forEach((updatedChat) => {
|
||||||
|
const chat = getChatById(state, updatedChat.id)
|
||||||
|
|
||||||
|
if (chat) {
|
||||||
|
chat.lastMessage = updatedChat.lastMessage
|
||||||
|
chat.unread = updatedChat.unread
|
||||||
|
} else {
|
||||||
|
state.chatList.data.push(updatedChat)
|
||||||
|
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
|
||||||
|
const chat = getChatById(state, updatedChat.id)
|
||||||
|
if (chat) {
|
||||||
|
chat.lastMessage = updatedChat.lastMessage
|
||||||
|
chat.unread = updatedChat.unread
|
||||||
|
chat.updated_at = updatedChat.updated_at
|
||||||
|
}
|
||||||
|
if (!chat) { state.chatList.data.unshift(updatedChat) }
|
||||||
|
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
|
||||||
|
},
|
||||||
|
deleteChat (state, { _dispatch, id, _rootGetters }) {
|
||||||
|
state.chats.data = state.chats.data.filter(conversation =>
|
||||||
|
conversation.last_status.id !== id
|
||||||
|
)
|
||||||
|
state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
|
||||||
|
},
|
||||||
|
resetChats (state, { commit }) {
|
||||||
|
state.chatList = emptyChatList()
|
||||||
|
state.currentChatId = null
|
||||||
|
commit('setChatListFetcher', { fetcher: undefined })
|
||||||
|
for (const chatId in state.openedChats) {
|
||||||
|
chatService.clear(state.openedChatMessageServices[chatId])
|
||||||
|
Vue.delete(state.openedChats, chatId)
|
||||||
|
Vue.delete(state.openedChatMessageServices, chatId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setChatsLoading (state, { value }) {
|
||||||
|
state.chats.loading = value
|
||||||
|
},
|
||||||
|
addChatMessages (state, { commit, chatId, messages }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
if (chatMessageService) {
|
||||||
|
chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
|
||||||
|
commit('refreshLastMessage', { chatId })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshLastMessage (state, { chatId }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
if (chatMessageService) {
|
||||||
|
const chat = getChatById(state, chatId)
|
||||||
|
if (chat) {
|
||||||
|
chat.lastMessage = chatMessageService.lastMessage
|
||||||
|
if (chatMessageService.lastMessage) {
|
||||||
|
chat.updated_at = chatMessageService.lastMessage.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteChatMessage (state, { commit, chatId, messageId }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
if (chatMessageService) {
|
||||||
|
chatService.deleteMessage(chatMessageService, messageId)
|
||||||
|
commit('refreshLastMessage', { chatId })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetChatNewMessageCount (state, _value) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[state.currentChatId]
|
||||||
|
chatService.resetNewMessageCount(chatMessageService)
|
||||||
|
},
|
||||||
|
// Used when a connection loss occurs
|
||||||
|
clearOpenedChats (state) {
|
||||||
|
const currentChatId = state.currentChatId
|
||||||
|
for (const chatId in state.openedChats) {
|
||||||
|
if (currentChatId !== chatId) {
|
||||||
|
chatService.clear(state.openedChatMessageServices[chatId])
|
||||||
|
Vue.delete(state.openedChats, chatId)
|
||||||
|
Vue.delete(state.openedChatMessageServices, chatId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readChat (state, { id }) {
|
||||||
|
const chat = getChatById(state, id)
|
||||||
|
if (chat) {
|
||||||
|
chat.unread = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default chats
|
|
@ -46,7 +46,8 @@ export const defaultState = {
|
||||||
repeats: true,
|
repeats: true,
|
||||||
moves: true,
|
moves: true,
|
||||||
emojiReactions: false,
|
emojiReactions: false,
|
||||||
followRequest: true
|
followRequest: true,
|
||||||
|
chatMention: true
|
||||||
},
|
},
|
||||||
webPushNotifications: false,
|
webPushNotifications: false,
|
||||||
muteWords: [],
|
muteWords: [],
|
||||||
|
|
|
@ -15,6 +15,8 @@ const defaultState = {
|
||||||
|
|
||||||
// Stuff from static/config.json
|
// Stuff from static/config.json
|
||||||
alwaysShowSubjectInput: true,
|
alwaysShowSubjectInput: true,
|
||||||
|
defaultAvatar: '/images/avi.png',
|
||||||
|
defaultBanner: '/images/banner.png',
|
||||||
background: '/static/aurora_borealis.jpg',
|
background: '/static/aurora_borealis.jpg',
|
||||||
collapseMessageWithSubject: false,
|
collapseMessageWithSubject: false,
|
||||||
disableChat: false,
|
disableChat: false,
|
||||||
|
@ -53,6 +55,7 @@ const defaultState = {
|
||||||
|
|
||||||
// Feature-set, apparently, not everything here is reported...
|
// Feature-set, apparently, not everything here is reported...
|
||||||
chatAvailable: false,
|
chatAvailable: false,
|
||||||
|
pleromaChatMessagesAvailable: false,
|
||||||
gopherAvailable: false,
|
gopherAvailable: false,
|
||||||
mediaProxyAvailable: false,
|
mediaProxyAvailable: false,
|
||||||
suggestionsEnabled: false,
|
suggestionsEnabled: false,
|
||||||
|
|
|
@ -14,7 +14,9 @@ const defaultState = {
|
||||||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
mobileLayout: false
|
mobileLayout: false,
|
||||||
|
globalNotices: [],
|
||||||
|
layoutHeight: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const interfaceMod = {
|
const interfaceMod = {
|
||||||
|
@ -58,6 +60,15 @@ const interfaceMod = {
|
||||||
if (!state.settingsModalLoaded) {
|
if (!state.settingsModalLoaded) {
|
||||||
state.settingsModalLoaded = true
|
state.settingsModalLoaded = true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
pushGlobalNotice (state, notice) {
|
||||||
|
state.globalNotices.push(notice)
|
||||||
|
},
|
||||||
|
removeGlobalNotice (state, notice) {
|
||||||
|
state.globalNotices = state.globalNotices.filter(n => n !== notice)
|
||||||
|
},
|
||||||
|
setLayoutHeight (state, value) {
|
||||||
|
state.layoutHeight = value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -81,6 +92,31 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
togglePeekSettingsModal ({ commit }) {
|
togglePeekSettingsModal ({ commit }) {
|
||||||
commit('togglePeekSettingsModal')
|
commit('togglePeekSettingsModal')
|
||||||
|
},
|
||||||
|
pushGlobalNotice (
|
||||||
|
{ commit, dispatch },
|
||||||
|
{
|
||||||
|
messageKey,
|
||||||
|
messageArgs = {},
|
||||||
|
level = 'error',
|
||||||
|
timeout = 0
|
||||||
|
}) {
|
||||||
|
const notice = {
|
||||||
|
messageKey,
|
||||||
|
messageArgs,
|
||||||
|
level
|
||||||
|
}
|
||||||
|
if (timeout) {
|
||||||
|
setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
|
||||||
|
}
|
||||||
|
commit('pushGlobalNotice', notice)
|
||||||
|
return notice
|
||||||
|
},
|
||||||
|
removeGlobalNotice ({ commit }, notice) {
|
||||||
|
commit('removeGlobalNotice', notice)
|
||||||
|
},
|
||||||
|
setLayoutHeight ({ commit }, value) {
|
||||||
|
commit('setLayoutHeight', value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const mediaViewer = {
|
||||||
setMedia ({ commit }, attachments) {
|
setMedia ({ commit }, attachments) {
|
||||||
const media = attachments.filter(attachment => {
|
const media = attachments.filter(attachment => {
|
||||||
const type = fileTypeService.fileType(attachment.mimetype)
|
const type = fileTypeService.fileType(attachment.mimetype)
|
||||||
return type === 'image' || type === 'video'
|
return type === 'image' || type === 'video' || type === 'audio'
|
||||||
})
|
})
|
||||||
commit('setMedia', media)
|
commit('setMedia', media)
|
||||||
},
|
},
|
||||||
|
|
|
@ -62,7 +62,8 @@ export const defaultState = () => ({
|
||||||
publicAndExternal: emptyTl(),
|
publicAndExternal: emptyTl(),
|
||||||
friends: emptyTl(),
|
friends: emptyTl(),
|
||||||
tag: emptyTl(),
|
tag: emptyTl(),
|
||||||
dms: emptyTl()
|
dms: emptyTl(),
|
||||||
|
bookmarks: emptyTl()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -163,8 +164,7 @@ const removeStatusFromGlobalStorage = (state, status) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {},
|
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
|
||||||
noIdUpdate = false, userId }) => {
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (!isArray(statuses)) {
|
if (!isArray(statuses)) {
|
||||||
return false
|
return false
|
||||||
|
@ -173,8 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
const allStatuses = state.allStatuses
|
const allStatuses = state.allStatuses
|
||||||
const timelineObject = state.timelines[timeline]
|
const timelineObject = state.timelines[timeline]
|
||||||
|
|
||||||
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
|
// Mismatch between API pagination and our internal minId/maxId tracking systems:
|
||||||
const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
|
// pagination.maxId is the oldest of the returned statuses when fetching older,
|
||||||
|
// and pagination.minId is the newest when fetching newer. The names come directly
|
||||||
|
// from the arguments they're supposed to be passed as for the next fetch.
|
||||||
|
const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
|
||||||
|
const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
|
||||||
|
|
||||||
const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
|
const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
|
||||||
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
|
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
|
||||||
|
|
||||||
|
@ -315,7 +320,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
})
|
})
|
||||||
|
|
||||||
// Keep the visible statuses sorted
|
// Keep the visible statuses sorted
|
||||||
if (timeline) {
|
if (timeline && !(timeline === 'bookmarks')) {
|
||||||
sortTimeline(timelineObject)
|
sortTimeline(timelineObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,9 +468,17 @@ export const mutations = {
|
||||||
newStatus.rebloggedBy.push(user)
|
newStatus.rebloggedBy.push(user)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setBookmarked (state, { status, value }) {
|
||||||
|
const newStatus = state.allStatusesObject[status.id]
|
||||||
|
newStatus.bookmarked = value
|
||||||
|
},
|
||||||
|
setBookmarkedConfirm (state, { status }) {
|
||||||
|
const newStatus = state.allStatusesObject[status.id]
|
||||||
|
newStatus.bookmarked = status.bookmarked
|
||||||
|
},
|
||||||
setDeleted (state, { status }) {
|
setDeleted (state, { status }) {
|
||||||
const newStatus = state.allStatusesObject[status.id]
|
const newStatus = state.allStatusesObject[status.id]
|
||||||
newStatus.deleted = true
|
if (newStatus) newStatus.deleted = true
|
||||||
},
|
},
|
||||||
setManyDeleted (state, condition) {
|
setManyDeleted (state, condition) {
|
||||||
Object.values(state.allStatusesObject).forEach(status => {
|
Object.values(state.allStatusesObject).forEach(status => {
|
||||||
|
@ -508,6 +521,9 @@ export const mutations = {
|
||||||
dismissNotification (state, { id }) {
|
dismissNotification (state, { id }) {
|
||||||
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
|
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
|
||||||
},
|
},
|
||||||
|
dismissNotifications (state, { finder }) {
|
||||||
|
state.notifications.data = state.notifications.data.filter(n => finder)
|
||||||
|
},
|
||||||
updateNotification (state, { id, updater }) {
|
updateNotification (state, { id, updater }) {
|
||||||
const notification = find(state.notifications.data, n => n.id === id)
|
const notification = find(state.notifications.data, n => n.id === id)
|
||||||
notification && updater(notification)
|
notification && updater(notification)
|
||||||
|
@ -515,6 +531,11 @@ export const mutations = {
|
||||||
queueFlush (state, { timeline, id }) {
|
queueFlush (state, { timeline, id }) {
|
||||||
state.timelines[timeline].flushMarker = id
|
state.timelines[timeline].flushMarker = id
|
||||||
},
|
},
|
||||||
|
queueFlushAll (state) {
|
||||||
|
Object.keys(state.timelines).forEach((timeline) => {
|
||||||
|
state.timelines[timeline].flushMarker = state.timelines[timeline].maxId
|
||||||
|
})
|
||||||
|
},
|
||||||
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
|
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
|
||||||
const newStatus = state.allStatusesObject[id]
|
const newStatus = state.allStatusesObject[id]
|
||||||
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
||||||
|
@ -585,8 +606,8 @@ export const mutations = {
|
||||||
const statuses = {
|
const statuses = {
|
||||||
state: defaultState(),
|
state: defaultState(),
|
||||||
actions: {
|
actions: {
|
||||||
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
|
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
|
||||||
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
|
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
|
||||||
},
|
},
|
||||||
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
||||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
||||||
|
@ -661,9 +682,26 @@ const statuses = {
|
||||||
rootState.api.backendInteractor.unretweet({ id: status.id })
|
rootState.api.backendInteractor.unretweet({ id: status.id })
|
||||||
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
|
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
|
||||||
},
|
},
|
||||||
|
bookmark ({ rootState, commit }, status) {
|
||||||
|
commit('setBookmarked', { status, value: true })
|
||||||
|
rootState.api.backendInteractor.bookmarkStatus({ id: status.id })
|
||||||
|
.then(status => {
|
||||||
|
commit('setBookmarkedConfirm', { status })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unbookmark ({ rootState, commit }, status) {
|
||||||
|
commit('setBookmarked', { status, value: false })
|
||||||
|
rootState.api.backendInteractor.unbookmarkStatus({ id: status.id })
|
||||||
|
.then(status => {
|
||||||
|
commit('setBookmarkedConfirm', { status })
|
||||||
|
})
|
||||||
|
},
|
||||||
queueFlush ({ rootState, commit }, { timeline, id }) {
|
queueFlush ({ rootState, commit }, { timeline, id }) {
|
||||||
commit('queueFlush', { timeline, id })
|
commit('queueFlush', { timeline, id })
|
||||||
},
|
},
|
||||||
|
queueFlushAll ({ rootState, commit }) {
|
||||||
|
commit('queueFlushAll')
|
||||||
|
},
|
||||||
markNotificationsAsSeen ({ rootState, commit }) {
|
markNotificationsAsSeen ({ rootState, commit }) {
|
||||||
commit('markNotificationsAsSeen')
|
commit('markNotificationsAsSeen')
|
||||||
apiService.markNotificationsAsSeen({
|
apiService.markNotificationsAsSeen({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import oauthApi from '../services/new_api/oauth.js'
|
import oauthApi from '../services/new_api/oauth.js'
|
||||||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
const oldItem = obj[item.id]
|
const oldItem = obj[item.id]
|
||||||
if (oldItem) {
|
if (oldItem) {
|
||||||
// We already have this, so only merge the new info.
|
// We already have this, so only merge the new info.
|
||||||
merge(oldItem, item)
|
mergeWith(oldItem, item, mergeArrayLength)
|
||||||
return { item: oldItem, new: false }
|
return { item: oldItem, new: false }
|
||||||
} else {
|
} else {
|
||||||
// This is a new item, prepare it
|
// This is a new item, prepare it
|
||||||
|
@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mergeArrayLength = (oldValue, newValue) => {
|
||||||
|
if (isArray(oldValue) && isArray(newValue)) {
|
||||||
|
oldValue.length = newValue.length
|
||||||
|
return mergeWith(oldValue, newValue, mergeArrayLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getNotificationPermission = () => {
|
const getNotificationPermission = () => {
|
||||||
const Notification = window.Notification
|
const Notification = window.Notification
|
||||||
|
|
||||||
|
@ -116,7 +123,7 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
setCurrentUser (state, user) {
|
setCurrentUser (state, user) {
|
||||||
state.lastLoginName = user.screen_name
|
state.lastLoginName = user.screen_name
|
||||||
state.currentUser = merge(state.currentUser || {}, user)
|
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
|
||||||
},
|
},
|
||||||
clearCurrentUser (state) {
|
clearCurrentUser (state) {
|
||||||
state.currentUser = false
|
state.currentUser = false
|
||||||
|
@ -259,6 +266,11 @@ const users = {
|
||||||
mutations,
|
mutations,
|
||||||
getters,
|
getters,
|
||||||
actions: {
|
actions: {
|
||||||
|
fetchUserIfMissing (store, id) {
|
||||||
|
if (!store.getters.findUser(id)) {
|
||||||
|
store.dispatch('fetchUser', id)
|
||||||
|
}
|
||||||
|
},
|
||||||
fetchUser (store, id) {
|
fetchUser (store, id) {
|
||||||
return store.rootState.api.backendInteractor.fetchUser({ id })
|
return store.rootState.api.backendInteractor.fetchUser({ id })
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
|
@ -486,6 +498,7 @@ const users = {
|
||||||
store.dispatch('stopFetchingFollowRequests')
|
store.dispatch('stopFetchingFollowRequests')
|
||||||
store.commit('clearNotifications')
|
store.commit('clearNotifications')
|
||||||
store.commit('resetStatuses')
|
store.commit('resetStatuses')
|
||||||
|
store.dispatch('resetChats')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loginUser (store, accessToken) {
|
loginUser (store, accessToken) {
|
||||||
|
@ -525,6 +538,9 @@ const users = {
|
||||||
|
|
||||||
// Start fetching notifications
|
// Start fetching notifications
|
||||||
store.dispatch('startFetchingNotifications')
|
store.dispatch('startFetchingNotifications')
|
||||||
|
|
||||||
|
// Start fetching chats
|
||||||
|
store.dispatch('startFetchingChats')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.getters.mergedConfig.useStreamingApi) {
|
if (store.getters.mergedConfig.useStreamingApi) {
|
||||||
|
@ -532,6 +548,7 @@ const users = {
|
||||||
console.error('Failed initializing MastoAPI Streaming socket', error)
|
console.error('Failed initializing MastoAPI Streaming socket', error)
|
||||||
startPolling()
|
startPolling()
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
store.dispatch('fetchChats', { latest: true })
|
||||||
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { each, map, concat, last, get } from 'lodash'
|
import { each, map, concat, last, get } from 'lodash'
|
||||||
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
|
import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
@ -50,6 +50,7 @@ const MASTODON_USER_URL = '/api/v1/accounts'
|
||||||
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
||||||
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
|
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
|
||||||
|
const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks'
|
||||||
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
|
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
|
||||||
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
||||||
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
||||||
|
@ -58,6 +59,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
|
||||||
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
|
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
|
||||||
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
|
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
|
||||||
const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
|
const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
|
||||||
|
const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark`
|
||||||
|
const MASTODON_UNBOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/unbookmark`
|
||||||
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
||||||
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||||
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
|
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
|
||||||
|
@ -78,6 +81,11 @@ const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
||||||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
||||||
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||||
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||||
|
const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
|
||||||
|
const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
|
||||||
|
const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
|
||||||
|
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
|
||||||
|
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||||
|
|
||||||
const oldfetch = window.fetch
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
|
@ -138,20 +146,11 @@ const updateNotificationSettings = ({ credentials, settings }) => {
|
||||||
}).then((data) => data.json())
|
}).then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAvatar = ({ credentials, avatar }) => {
|
const updateProfileImages = ({ credentials, avatar = null, banner = null, background = null }) => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('avatar', avatar)
|
if (avatar !== null) form.append('avatar', avatar)
|
||||||
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
if (banner !== null) form.append('header', banner)
|
||||||
headers: authHeaders(credentials),
|
if (background !== null) form.append('pleroma_background_image', background)
|
||||||
method: 'PATCH',
|
|
||||||
body: form
|
|
||||||
}).then((data) => data.json())
|
|
||||||
.then((data) => parseUser(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateBg = ({ credentials, background }) => {
|
|
||||||
const form = new FormData()
|
|
||||||
form.append('pleroma_background_image', background)
|
|
||||||
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
@ -161,17 +160,6 @@ const updateBg = ({ credentials, background }) => {
|
||||||
.then((data) => parseUser(data))
|
.then((data) => parseUser(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateBanner = ({ credentials, banner }) => {
|
|
||||||
const form = new FormData()
|
|
||||||
form.append('header', banner)
|
|
||||||
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
|
||||||
headers: authHeaders(credentials),
|
|
||||||
method: 'PATCH',
|
|
||||||
body: form
|
|
||||||
}).then((data) => data.json())
|
|
||||||
.then((data) => parseUser(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateProfile = ({ credentials, params }) => {
|
const updateProfile = ({ credentials, params }) => {
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: MASTODON_PROFILE_UPDATE_URL,
|
url: MASTODON_PROFILE_UPDATE_URL,
|
||||||
|
@ -498,7 +486,8 @@ const fetchTimeline = ({
|
||||||
until = false,
|
until = false,
|
||||||
userId = false,
|
userId = false,
|
||||||
tag = false,
|
tag = false,
|
||||||
withMuted = false
|
withMuted = false,
|
||||||
|
replyVisibility = 'all'
|
||||||
}) => {
|
}) => {
|
||||||
const timelineUrls = {
|
const timelineUrls = {
|
||||||
public: MASTODON_PUBLIC_TIMELINE,
|
public: MASTODON_PUBLIC_TIMELINE,
|
||||||
|
@ -509,7 +498,8 @@ const fetchTimeline = ({
|
||||||
user: MASTODON_USER_TIMELINE_URL,
|
user: MASTODON_USER_TIMELINE_URL,
|
||||||
media: MASTODON_USER_TIMELINE_URL,
|
media: MASTODON_USER_TIMELINE_URL,
|
||||||
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||||
tag: MASTODON_TAG_TIMELINE_URL
|
tag: MASTODON_TAG_TIMELINE_URL,
|
||||||
|
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
|
||||||
}
|
}
|
||||||
const isNotifications = timeline === 'notifications'
|
const isNotifications = timeline === 'notifications'
|
||||||
const params = []
|
const params = []
|
||||||
|
@ -538,9 +528,12 @@ const fetchTimeline = ({
|
||||||
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||||
params.push(['only_media', false])
|
params.push(['only_media', false])
|
||||||
}
|
}
|
||||||
if (timeline !== 'favorites') {
|
if (timeline !== 'favorites' && timeline !== 'bookmarks') {
|
||||||
params.push(['with_muted', withMuted])
|
params.push(['with_muted', withMuted])
|
||||||
}
|
}
|
||||||
|
if (replyVisibility !== 'all') {
|
||||||
|
params.push(['reply_visibility', replyVisibility])
|
||||||
|
}
|
||||||
|
|
||||||
params.push(['limit', 20])
|
params.push(['limit', 20])
|
||||||
|
|
||||||
|
@ -548,16 +541,20 @@ const fetchTimeline = ({
|
||||||
url += `?${queryString}`
|
url += `?${queryString}`
|
||||||
let status = ''
|
let status = ''
|
||||||
let statusText = ''
|
let statusText = ''
|
||||||
|
let pagination = {}
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
status = data.status
|
status = data.status
|
||||||
statusText = data.statusText
|
statusText = data.statusText
|
||||||
|
pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
|
||||||
|
flakeId: timeline !== 'bookmarks' && timeline !== 'notifications'
|
||||||
|
})
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
return data.map(isNotifications ? parseNotification : parseStatus)
|
return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
|
||||||
} else {
|
} else {
|
||||||
data.status = status
|
data.status = status
|
||||||
data.statusText = statusText
|
data.statusText = statusText
|
||||||
|
@ -608,6 +605,22 @@ const unretweet = ({ id, credentials }) => {
|
||||||
.then((data) => parseStatus(data))
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bookmarkStatus = ({ id, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_BOOKMARK_STATUS_URL(id),
|
||||||
|
headers: authHeaders(credentials),
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unbookmarkStatus = ({ id, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_UNBOOKMARK_STATUS_URL(id),
|
||||||
|
headers: authHeaders(credentials),
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const postStatus = ({
|
const postStatus = ({
|
||||||
credentials,
|
credentials,
|
||||||
status,
|
status,
|
||||||
|
@ -617,7 +630,8 @@ const postStatus = ({
|
||||||
poll,
|
poll,
|
||||||
mediaIds = [],
|
mediaIds = [],
|
||||||
inReplyToStatusId,
|
inReplyToStatusId,
|
||||||
contentType
|
contentType,
|
||||||
|
preview
|
||||||
}) => {
|
}) => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
const pollOptions = poll.options || []
|
const pollOptions = poll.options || []
|
||||||
|
@ -647,6 +661,9 @@ const postStatus = ({
|
||||||
if (inReplyToStatusId) {
|
if (inReplyToStatusId) {
|
||||||
form.append('in_reply_to_id', inReplyToStatusId)
|
form.append('in_reply_to_id', inReplyToStatusId)
|
||||||
}
|
}
|
||||||
|
if (preview) {
|
||||||
|
form.append('preview', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(MASTODON_POST_STATUS_URL, {
|
return fetch(MASTODON_POST_STATUS_URL, {
|
||||||
body: form,
|
body: form,
|
||||||
|
@ -654,13 +671,7 @@ const postStatus = ({
|
||||||
headers: authHeaders(credentials)
|
headers: authHeaders(credentials)
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok) {
|
return response.json()
|
||||||
return response.json()
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
error: response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.then((data) => data.error ? data : parseStatus(data))
|
.then((data) => data.error ? data : parseStatus(data))
|
||||||
}
|
}
|
||||||
|
@ -682,6 +693,17 @@ const uploadMedia = ({ formData, credentials }) => {
|
||||||
.then((data) => parseAttachment(data))
|
.then((data) => parseAttachment(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setMediaDescription = ({ id, description, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: authHeaders(credentials),
|
||||||
|
payload: {
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}).then((data) => parseAttachment(data))
|
||||||
|
}
|
||||||
|
|
||||||
const importBlocks = ({ file, credentials }) => {
|
const importBlocks = ({ file, credentials }) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('list', file)
|
formData.append('list', file)
|
||||||
|
@ -1050,6 +1072,10 @@ const MASTODON_STREAMING_EVENTS = new Set([
|
||||||
'filters_changed'
|
'filters_changed'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const PLEROMA_STREAMING_EVENTS = new Set([
|
||||||
|
'pleroma:chat_update'
|
||||||
|
])
|
||||||
|
|
||||||
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
||||||
// Uses EventTarget and a CustomEvent to proxy events
|
// Uses EventTarget and a CustomEvent to proxy events
|
||||||
export const ProcessedWS = ({
|
export const ProcessedWS = ({
|
||||||
|
@ -1106,7 +1132,7 @@ export const handleMastoWS = (wsEvent) => {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
const parsedEvent = JSON.parse(data)
|
const parsedEvent = JSON.parse(data)
|
||||||
const { event, payload } = parsedEvent
|
const { event, payload } = parsedEvent
|
||||||
if (MASTODON_STREAMING_EVENTS.has(event)) {
|
if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) {
|
||||||
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
|
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
|
||||||
if (event === 'delete') {
|
if (event === 'delete') {
|
||||||
return { event, id: payload }
|
return { event, id: payload }
|
||||||
|
@ -1116,6 +1142,8 @@ export const handleMastoWS = (wsEvent) => {
|
||||||
return { event, status: parseStatus(data) }
|
return { event, status: parseStatus(data) }
|
||||||
} else if (event === 'notification') {
|
} else if (event === 'notification') {
|
||||||
return { event, notification: parseNotification(data) }
|
return { event, notification: parseNotification(data) }
|
||||||
|
} else if (event === 'pleroma:chat_update') {
|
||||||
|
return { event, chatUpdate: parseChat(data) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Unknown event', wsEvent)
|
console.warn('Unknown event', wsEvent)
|
||||||
|
@ -1123,6 +1151,81 @@ export const handleMastoWS = (wsEvent) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WSConnectionStatus = Object.freeze({
|
||||||
|
'JOINED': 1,
|
||||||
|
'CLOSED': 2,
|
||||||
|
'ERROR': 3
|
||||||
|
})
|
||||||
|
|
||||||
|
const chats = ({ credentials }) => {
|
||||||
|
return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) })
|
||||||
|
.then((data) => data.json())
|
||||||
|
.then((data) => {
|
||||||
|
return { chats: data.map(parseChat).filter(c => c) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrCreateChat = ({ accountId, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_URL(accountId),
|
||||||
|
method: 'POST',
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
|
||||||
|
let url = PLEROMA_CHAT_MESSAGES_URL(id)
|
||||||
|
const args = [
|
||||||
|
maxId && `max_id=${maxId}`,
|
||||||
|
sinceId && `since_id=${sinceId}`,
|
||||||
|
limit && `limit=${limit}`
|
||||||
|
].filter(_ => _).join('&')
|
||||||
|
|
||||||
|
url = url + (args ? '?' + args : '')
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
|
||||||
|
const payload = {
|
||||||
|
'content': content
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaId) {
|
||||||
|
payload['media_id'] = mediaId
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
payload: payload,
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readChat = ({ id, lastReadId, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_CHAT_READ_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
'last_read_id': lastReadId
|
||||||
|
},
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteChatMessage = ({ chatId, messageId, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const apiService = {
|
const apiService = {
|
||||||
verifyCredentials,
|
verifyCredentials,
|
||||||
fetchTimeline,
|
fetchTimeline,
|
||||||
|
@ -1146,9 +1249,12 @@ const apiService = {
|
||||||
unfavorite,
|
unfavorite,
|
||||||
retweet,
|
retweet,
|
||||||
unretweet,
|
unretweet,
|
||||||
|
bookmarkStatus,
|
||||||
|
unbookmarkStatus,
|
||||||
postStatus,
|
postStatus,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
uploadMedia,
|
uploadMedia,
|
||||||
|
setMediaDescription,
|
||||||
fetchMutes,
|
fetchMutes,
|
||||||
muteUser,
|
muteUser,
|
||||||
unmuteUser,
|
unmuteUser,
|
||||||
|
@ -1166,10 +1272,8 @@ const apiService = {
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
register,
|
register,
|
||||||
getCaptcha,
|
getCaptcha,
|
||||||
updateAvatar,
|
updateProfileImages,
|
||||||
updateBg,
|
|
||||||
updateProfile,
|
updateProfile,
|
||||||
updateBanner,
|
|
||||||
importBlocks,
|
importBlocks,
|
||||||
importFollows,
|
importFollows,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
|
@ -1200,7 +1304,13 @@ const apiService = {
|
||||||
fetchKnownDomains,
|
fetchKnownDomains,
|
||||||
fetchDomainMutes,
|
fetchDomainMutes,
|
||||||
muteDomain,
|
muteDomain,
|
||||||
unmuteDomain
|
unmuteDomain,
|
||||||
|
chats,
|
||||||
|
getOrCreateChat,
|
||||||
|
chatMessages,
|
||||||
|
sendChatMessage,
|
||||||
|
readChat,
|
||||||
|
deleteChatMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -12,10 +12,6 @@ const backendInteractorService = credentials => ({
|
||||||
return notificationsFetcher.startFetching({ store, credentials })
|
return notificationsFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAndUpdateNotifications ({ store }) {
|
|
||||||
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
|
||||||
},
|
|
||||||
|
|
||||||
startFetchingFollowRequests ({ store }) {
|
startFetchingFollowRequests ({ store }) {
|
||||||
return followRequestFetcher.startFetching({ store, credentials })
|
return followRequestFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue