Merge remote-tracking branch 'upstream/develop' into birthdays
This commit is contained in:
commit
b1e75c25bd
300 changed files with 13879 additions and 9055 deletions
17
src/App.js
17
src/App.js
|
|
@ -10,7 +10,9 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
|
|||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
|
@ -32,8 +34,11 @@ export default {
|
|||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
|
|
@ -59,6 +64,13 @@ export default {
|
|||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
navClasses () {
|
||||
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||
return [
|
||||
'-' + this.layoutType,
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||
]
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
|
|
@ -84,11 +96,16 @@ export default {
|
|||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit () {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
},
|
||||
|
|
|
|||
139
src/App.scss
139
src/App.scss
|
|
@ -1,16 +1,18 @@
|
|||
// stylelint-disable rscss/class-format
|
||||
@import './_variables.scss';
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
@import "./variables";
|
||||
@import "./panel";
|
||||
|
||||
:root {
|
||||
--navbar-height: 3.5rem;
|
||||
--post-line-height: 1.4;
|
||||
// Z-Index stuff
|
||||
--ZI_media_modal: 90000;
|
||||
--ZI_modals_popovers: 85000;
|
||||
--ZI_modals: 80000;
|
||||
--ZI_navbar_popovers: 75000;
|
||||
--ZI_navbar: 70000;
|
||||
--ZI_popovers: 60000;
|
||||
--ZI_media_modal: 9000;
|
||||
--ZI_modals_popovers: 8500;
|
||||
--ZI_modals: 8000;
|
||||
--ZI_navbar_popovers: 7500;
|
||||
--ZI_navbar: 7000;
|
||||
--ZI_popovers: 6000;
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
@ -117,20 +119,35 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
.iconLetter {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
i[class*="icon-"],
|
||||
.svg-inline--fa,
|
||||
.iconLetter {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
}
|
||||
|
||||
.button-unstyled:hover,
|
||||
a:hover {
|
||||
> i[class*="icon-"],
|
||||
> .svg-inline--fa,
|
||||
> .iconLetter {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
z-index: var(--ZI_navbar);
|
||||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: 0 0 4px rgb(0 0 0 / 60%);
|
||||
box-shadow: var(--topBarShadow);
|
||||
box-sizing: border-box;
|
||||
height: var(--navbar-height);
|
||||
|
|
@ -141,6 +158,11 @@ nav {
|
|||
grid-area: sidebar;
|
||||
}
|
||||
|
||||
#modal {
|
||||
position: absolute;
|
||||
z-index: var(--ZI_modals);
|
||||
}
|
||||
|
||||
.column.-scrollable {
|
||||
top: var(--navbar-height);
|
||||
position: sticky;
|
||||
|
|
@ -170,25 +192,28 @@ nav {
|
|||
}
|
||||
|
||||
.underlay {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: span 3;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 1;
|
||||
grid-column: 1 / span 3;
|
||||
grid-row: 1 / 1;
|
||||
pointer-events: none;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
|
||||
background-color: rgb(0 0 0 / 15%);
|
||||
background-color: var(--underlay, rgb(0 0 0 / 15%));
|
||||
z-index: -1000;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
--miniColumn: 25rem;
|
||||
--maxiColumn: minmax(var(--miniColumn), 45rem);
|
||||
--maxiColumn: 45rem;
|
||||
--columnGap: 1em;
|
||||
--status-margin: 0.75em;
|
||||
--effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
|
||||
--effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
|
||||
--effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));
|
||||
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
||||
grid-template-columns:
|
||||
var(--effectiveSidebarColumnWidth)
|
||||
var(--effectiveContentColumnWidth);
|
||||
grid-template-areas: "sidebar content";
|
||||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -205,8 +230,7 @@ nav {
|
|||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
box-sizing: border-box;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 1;
|
||||
grid-row: 1 / 1;
|
||||
margin: 0 calc(var(--___columnMargin) / 2);
|
||||
padding: calc(var(--___columnMargin)) 0;
|
||||
row-gap: var(--___columnMargin);
|
||||
|
|
@ -281,16 +305,25 @@ nav {
|
|||
align-content: start;
|
||||
}
|
||||
|
||||
&.-reverse:not(.-wide):not(.-mobile) {
|
||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
||||
&.-reverse:not(.-wide, .-mobile) {
|
||||
grid-template-columns:
|
||||
var(--effectiveContentColumnWidth)
|
||||
var(--effectiveSidebarColumnWidth);
|
||||
grid-template-areas: "content sidebar";
|
||||
}
|
||||
|
||||
&.-wide {
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-columns:
|
||||
var(--effectiveSidebarColumnWidth)
|
||||
var(--effectiveContentColumnWidth)
|
||||
var(--effectiveNotifsColumnWidth);
|
||||
grid-template-areas: "sidebar content notifs";
|
||||
|
||||
&.-reverse {
|
||||
grid-template-columns:
|
||||
var(--effectiveNotifsColumnWidth)
|
||||
var(--effectiveContentColumnWidth)
|
||||
var(--effectiveSidebarColumnWidth);
|
||||
grid-template-areas: "notifs content sidebar";
|
||||
}
|
||||
}
|
||||
|
|
@ -301,11 +334,8 @@ nav {
|
|||
padding: 0;
|
||||
|
||||
.column {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-top: 0;
|
||||
margin-top: var(--navbar-height);
|
||||
margin-bottom: 0;
|
||||
margin: var(--navbar-height) 0 0 0;
|
||||
}
|
||||
|
||||
.panel-heading,
|
||||
|
|
@ -354,7 +384,7 @@ nav {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*="icon-"],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
|
|
@ -365,12 +395,15 @@ nav {
|
|||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 0 4px rgb(255 255 255 / 30%);
|
||||
box-shadow: var(--buttonHoverShadow);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow:
|
||||
0 0 4px 0 rgb(255 255 255 / 30%),
|
||||
0 1px 0 0 rgb(0 0 0 / 20%) inset,
|
||||
0 -1px 0 0 rgb(255 255 255 / 20%) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
|
|
@ -403,7 +436,10 @@ nav {
|
|||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow:
|
||||
0 0 4px 0 rgb(255 255 255 / 30%),
|
||||
0 1px 0 0 rgb(0 0 0 / 20%) inset,
|
||||
0 -1px 0 0 rgb(255 255 255 / 20%) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
|
||||
svg,
|
||||
|
|
@ -468,7 +504,10 @@ textarea,
|
|||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
|
||||
box-shadow:
|
||||
0 1px 0 0 rgb(0 0 0 / 20%) inset,
|
||||
0 -1px 0 0 rgb(255 255 255 / 20%) inset,
|
||||
0 0 2px 0 rgb(0 0 0 / 100%) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
|
@ -486,13 +525,13 @@ textarea,
|
|||
padding: 0 var(--_padding);
|
||||
|
||||
&:disabled,
|
||||
&[disabled=disabled],
|
||||
&[disabled="disabled"],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[type=range] {
|
||||
&[type="range"] {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
|
@ -500,7 +539,7 @@ textarea,
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
&[type=radio] {
|
||||
&[type="radio"] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
|
@ -520,7 +559,7 @@ textarea,
|
|||
+ label::before {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
content: "";
|
||||
transition: box-shadow 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
|
|
@ -540,7 +579,7 @@ textarea,
|
|||
}
|
||||
}
|
||||
|
||||
&[type=checkbox] {
|
||||
&[type="checkbox"] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
|
@ -559,7 +598,7 @@ textarea,
|
|||
+ label::before {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
content: '✓';
|
||||
content: "✓";
|
||||
transition: color 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
|
|
@ -599,10 +638,10 @@ option {
|
|||
}
|
||||
|
||||
.hide-number-spinner {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
|
||||
&[type=number]::-webkit-inner-spin-button,
|
||||
&[type=number]::-webkit-outer-spin-button {
|
||||
&[type="number"]::-webkit-inner-spin-button,
|
||||
&[type="number"]::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -634,8 +673,6 @@ option {
|
|||
}
|
||||
}
|
||||
|
||||
@import './panel.scss';
|
||||
|
||||
.fa {
|
||||
color: grey;
|
||||
}
|
||||
|
|
@ -651,7 +688,7 @@ option {
|
|||
max-width: 10em;
|
||||
min-width: 1.7em;
|
||||
height: 1.3em;
|
||||
padding: 0.15em 0.15em;
|
||||
padding: 0.15em;
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
|
@ -746,17 +783,24 @@ option {
|
|||
}
|
||||
|
||||
.fa-scale-110 {
|
||||
&.svg-inline--fa {
|
||||
&.svg-inline--fa,
|
||||
&.iconLetter {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-old-padding {
|
||||
&.svg-inline--fa {
|
||||
&.iconLetter,
|
||||
&.svg-inline--fa,
|
||||
&-layer {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.veryfaint {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
|
|
@ -842,3 +886,4 @@ option {
|
|||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
|
|
|||
15
src/App.vue
15
src/App.vue
|
|
@ -8,7 +8,10 @@
|
|||
class="app-bg-wrapper"
|
||||
/>
|
||||
<MobileNav v-if="layoutType === 'mobile'" />
|
||||
<DesktopNav v-else />
|
||||
<DesktopNav
|
||||
v-else
|
||||
:class="navClasses"
|
||||
/>
|
||||
<Notifications v-if="currentUser" />
|
||||
<div
|
||||
id="content"
|
||||
|
|
@ -30,10 +33,10 @@
|
|||
<div id="notifs-sidebar" />
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
<main
|
||||
id="main-scroller"
|
||||
class="column main"
|
||||
:class="{ '-full-height': isChats }"
|
||||
:class="{ '-full-height': isChats || isListEdit }"
|
||||
>
|
||||
<div
|
||||
v-if="!currentUser"
|
||||
|
|
@ -47,7 +50,7 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
<div
|
||||
id="notifs-column"
|
||||
class="column -scrollable"
|
||||
|
|
@ -64,10 +67,12 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<div id="modal" />
|
||||
<GlobalNoticeList />
|
||||
<div id="popovers" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
18
src/_mixins.scss
Normal file
18
src/_mixins.scss
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
@mixin unfocused-style {
|
||||
@content;
|
||||
|
||||
&:focus:not(:focus-visible, :hover) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin focused-style {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@content;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,20 +4,20 @@ $darkened-background: whitesmoke;
|
|||
|
||||
$fallback--bg: #121a24;
|
||||
$fallback--fg: #182230;
|
||||
$fallback--faint: rgba(185, 185, 186, .5);
|
||||
$fallback--faint: rgb(185 185 186 / 50%);
|
||||
$fallback--text: #b9b9ba;
|
||||
$fallback--link: #d8a070;
|
||||
$fallback--icon: #666;
|
||||
$fallback--lightBg: rgb(21, 30, 42);
|
||||
$fallback--lightBg: rgb(21 30 42);
|
||||
$fallback--lightText: #b9b9ba;
|
||||
$fallback--border: #222;
|
||||
$fallback--cRed: #ff0000;
|
||||
$fallback--cRed: #f00;
|
||||
$fallback--cBlue: #0095ff;
|
||||
$fallback--cGreen: #0fa00f;
|
||||
$fallback--cOrange: orange;
|
||||
|
||||
$fallback--alertError: rgba(211,16,20,.5);
|
||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
||||
$fallback--alertError: rgb(211 16 20 / 50%);
|
||||
$fallback--alertWarning: rgb(111 111 20 / 50%);
|
||||
|
||||
$fallback--panelRadius: 10px;
|
||||
$fallback--checkboxRadius: 2px;
|
||||
|
|
@ -29,6 +29,8 @@ $fallback--avatarAltRadius: 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: 0 0 2px 0 rgb(0 0 0 / 100%),
|
||||
0 1px 0 0 rgb(255 255 255 / 20%) inset,
|
||||
0 -1px 0 0 rgb(0 0 0 / 20%) inset;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
|
|
|||
BIN
src/assets/pleromatan_apology.png
Normal file
BIN
src/assets/pleromatan_apology.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 396 KiB |
BIN
src/assets/pleromatan_apology_fox.png
Normal file
BIN
src/assets/pleromatan_apology_fox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 521 KiB |
BIN
src/assets/pleromatan_apology_fox_mask.png
Normal file
BIN
src/assets/pleromatan_apology_fox_mask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/pleromatan_apology_mask.png
Normal file
BIN
src/assets/pleromatan_apology_mask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -1,6 +1,8 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
|
|
@ -12,7 +14,7 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils
|
|||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||
import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
|
@ -251,6 +253,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
|
||||
|
|
@ -360,6 +363,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
console.error('Failed to load any theme!')
|
||||
}
|
||||
|
||||
applyConfig(store.state.config)
|
||||
|
||||
// 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([
|
||||
|
|
@ -371,6 +376,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
store.dispatch('startFetchingAnnouncements')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
|
|
@ -393,6 +399,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
|
||||
app.use(vClickOutside)
|
||||
app.use(VBodyScrollLock)
|
||||
app.use(VueVirtualScroller)
|
||||
|
||||
app.component('FAIcon', FontAwesomeIcon)
|
||||
app.component('FALayers', FontAwesomeLayers)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
|||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
import About from 'components/about/about.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
|
@ -58,7 +63,7 @@ export default (store) => {
|
|||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
|
|
@ -72,7 +77,14 @@ export default (store) => {
|
|||
{ 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: 'about', path: '/about', component: About },
|
||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
||||
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
||||
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }
|
||||
]
|
||||
|
||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,3 @@
|
|||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
|
|
@ -19,7 +20,8 @@ const AccountActions = {
|
|||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover
|
||||
Popover,
|
||||
UserListMenu
|
||||
},
|
||||
methods: {
|
||||
showRepeats () {
|
||||
|
|
@ -34,6 +36,9 @@ const AccountActions = {
|
|||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@
|
|||
class="dropdown-divider"
|
||||
/>
|
||||
</template>
|
||||
<UserListMenu :user="user" />
|
||||
<button
|
||||
v-if="relationship.followed_by"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
|
|
@ -72,7 +80,8 @@
|
|||
<script src="./account_actions.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.AccountActions {
|
||||
.ellipsis-button {
|
||||
width: 2.5em;
|
||||
|
|
|
|||
108
src/components/announcement/announcement.js
Normal file
108
src/components/announcement/announcement.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { mapState } from 'vuex'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined
|
||||
},
|
||||
editError: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
},
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt () {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt () {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt () {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement () {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate (time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode () {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
|
||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit () {
|
||||
this.editing = false
|
||||
},
|
||||
clearError () {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Announcement
|
||||
136
src/components/announcement/announcement.vue
Normal file
136
src/components/announcement/announcement.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div class="announcement">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.title') }}</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
<rich-content
|
||||
v-if="!editing"
|
||||
:html="content"
|
||||
:emoji="announcement.emojis"
|
||||
:handle-links="true"
|
||||
/>
|
||||
<announcement-editor
|
||||
v-else
|
||||
:announcement="editedAnnouncement"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="times"
|
||||
>
|
||||
<span v-if="publishedAt">
|
||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||
</span>
|
||||
<span v-if="startsAt">
|
||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||
</span>
|
||||
<span v-if="endsAt">
|
||||
{{ $t('announcements.end_time_display', { time: endsAt }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="btn button-default"
|
||||
:class="{ toggled: isRead }"
|
||||
:disabled="inactive"
|
||||
:title="inactive ? $t('announcements.inactive_message') : ''"
|
||||
@click="markAsRead"
|
||||
>
|
||||
{{ $t('announcements.mark_as_read_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canEditAnnouncement"
|
||||
class="btn button-default"
|
||||
@click="enterEditMode"
|
||||
>
|
||||
{{ $t('announcements.edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canEditAnnouncement"
|
||||
class="btn button-default"
|
||||
@click="deleteAnnouncement"
|
||||
>
|
||||
{{ $t('announcements.delete_action') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="submitEdit"
|
||||
>
|
||||
{{ $t('announcements.submit_edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ $t('announcements.cancel_edit_action') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="editing && editError"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('announcements.edit_error', { error }) }}
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
:title="$t('announcements.close_error')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.announcement {
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.times {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.footer .actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
margin: 1em;
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
src/components/announcement_editor/announcement_editor.js
Normal file
13
src/components/announcement_editor/announcement_editor.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
||||
60
src/components/announcement_editor/announcement_editor.vue
Normal file
60
src/components/announcement_editor/announcement_editor.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="announcement-editor">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="announcement.content"
|
||||
class="post-textarea"
|
||||
rows="1"
|
||||
cols="1"
|
||||
:placeholder="$t('announcements.post_placeholder')"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-start-time"
|
||||
v-model="announcement.startsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-end-time"
|
||||
v-model="announcement.endsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<Checkbox
|
||||
id="announcement-all-day"
|
||||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcement_editor.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.announcement-editor {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
.announcement-metadata {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: none;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/components/announcements_page/announcements_page.js
Normal file
58
src/components/announcements_page/announcements_page.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { mapState } from 'vuex'
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false
|
||||
},
|
||||
posting: false,
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
},
|
||||
canPostAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement () {
|
||||
this.posting = true
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
||||
80
src/components/announcements_page/announcements_page.vue
Normal file
80
src/components/announcements_page/announcements_page.vue
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div class="panel panel-default announcements-page">
|
||||
<div class="panel-heading">
|
||||
<span>
|
||||
{{ $t('announcements.page_header') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
v-if="canPostAnnouncement"
|
||||
>
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
<announcement-editor
|
||||
:announcement="newAnnouncement"
|
||||
:disabled="posting"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
class="btn button-default post-button"
|
||||
:disabled="posting"
|
||||
@click.prevent="postAnnouncement"
|
||||
>
|
||||
{{ $t('announcements.post_action') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('announcements.post_error', { error }) }}
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
:title="$t('announcements.close_error')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcements_page.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.announcements-page {
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.post-button {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -34,9 +34,10 @@ export default {
|
|||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.btn {
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
margin: 0.5em;
|
||||
padding: 0.5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ library.add(
|
|||
const Attachment = {
|
||||
props: [
|
||||
'attachment',
|
||||
'compact',
|
||||
'description',
|
||||
'hideDescription',
|
||||
'nsfw',
|
||||
|
|
@ -71,7 +72,8 @@ const Attachment = {
|
|||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact
|
||||
},
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
|
|
@ -129,6 +131,9 @@ const Attachment = {
|
|||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Attachment {
|
||||
display: inline-flex;
|
||||
|
|
@ -102,14 +102,13 @@
|
|||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
|
|
@ -135,18 +134,32 @@
|
|||
margin-left: 0.5em;
|
||||
font-size: 1.25em;
|
||||
// TODO: theming? hard to theme with unknown background image color
|
||||
background: rgba(230, 230, 230, 0.7);
|
||||
background: rgb(230 230 230 / 70%);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
&:hover .svg-inline--fa {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
color: rgb(0 0 0 / 90%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-contain-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.-cover-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.oembed-container {
|
||||
line-height: 1.2em;
|
||||
flex: 1 0 100%;
|
||||
|
|
@ -160,8 +173,9 @@
|
|||
|
||||
.image {
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
border: 0px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
|
@ -172,9 +186,10 @@
|
|||
flex: 2;
|
||||
margin: 8px;
|
||||
word-break: break-all;
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -252,17 +267,9 @@
|
|||
cursor: progress;
|
||||
}
|
||||
|
||||
&.-contain-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.-cover-fit {
|
||||
img,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
&.-compact {
|
||||
.placeholder-container {
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,10 +162,11 @@
|
|||
target="_blank"
|
||||
>
|
||||
<FAIcon
|
||||
size="5x"
|
||||
:size="compact ? '2x' : '5x'"
|
||||
:icon="placeholderIconClass"
|
||||
:title="localDescription"
|
||||
/>
|
||||
<p>
|
||||
<p v-if="!compact">
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<script src="./autosuggest.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.autosuggest {
|
||||
position: relative;
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);
|
||||
box-shadow: var(--panelShadow);
|
||||
overflow-y: auto;
|
||||
z-index: 1;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<script src="./avatar_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
|
@ -10,7 +11,8 @@ const BasicUserCard = {
|
|||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
|
|
|
|||
|
|
@ -30,12 +30,10 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
<user-link
|
||||
class="basic-user-card-screen-name"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
@{{ user.screen_name_ui }}
|
||||
</router-link>
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
@ -51,7 +49,7 @@
|
|||
margin: 0;
|
||||
padding: 0.6em 1em;
|
||||
|
||||
--emoji-size: 14px;
|
||||
--emoji-size: 14px;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
.block-card-content-container {
|
||||
margin-top: 0.5em;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const Chat = {
|
|||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.$store.dispatch('clearCurrentChat')
|
||||
},
|
||||
|
|
@ -135,7 +136,7 @@ const Chat = {
|
|||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
handleResize (opts = {}) {
|
||||
const { expand = false, delayed = false } = opts
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
setTimeout(() => {
|
||||
|
|
@ -146,10 +147,10 @@ const Chat = {
|
|||
|
||||
this.$nextTick(() => {
|
||||
const { offsetHeight = undefined } = getScrollPosition()
|
||||
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
||||
if (diff !== 0 || (!this.bottomedOut() && expand)) {
|
||||
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
|
||||
if (diff !== 0 && !this.bottomedOut()) {
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({ top: window.scrollY + diff })
|
||||
window.scrollBy({ top: -Math.trunc(diff) })
|
||||
})
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
|
|
@ -187,6 +188,7 @@ const Chat = {
|
|||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) { return }
|
||||
|
||||
if (this.reachedTop()) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
width: 100%;
|
||||
overflow: visible;
|
||||
min-height: calc(100vh - var(--navbar-height));
|
||||
margin: 0 0 0 0;
|
||||
margin: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
|
||||
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 1px 1px rgb(0 0 0 / 30%), 0 2px 4px rgb(0 0 0 / 30%);
|
||||
z-index: 10;
|
||||
transition: 0.35s all;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,6 @@
|
|||
|
||||
<script src="./chat.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import './chat.scss';
|
||||
@import "../../variables";
|
||||
@import "./chat";
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
<script src="./chat_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.chat-list {
|
||||
min-height: 25em;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
&:hover {
|
||||
background-color: var(--selectedPost, $fallback--lightBg);
|
||||
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 3px 1px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.chat-list-item-left {
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
@ -79,13 +80,11 @@
|
|||
|
||||
.chat-preview-body {
|
||||
--emoji-size: 1.4em;
|
||||
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.time-wrapper {
|
||||
line-height: var(--post-line-height);
|
||||
}
|
||||
|
||||
.chat-preview-body {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,6 @@
|
|||
<script src="./chat_list_item.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import './chat_list_item.scss';
|
||||
@import "../../variables";
|
||||
@import "./chat_list_item";
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.chat-message-wrapper {
|
||||
|
||||
&.hovered-message-chain {
|
||||
.animated.Avatar {
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
@ -28,7 +28,8 @@
|
|||
.menu-icon {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, .extra-button-popover.open & {
|
||||
&:hover,
|
||||
.extra-button-popover.open & {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
|
@ -54,27 +55,11 @@
|
|||
width: 32px;
|
||||
}
|
||||
|
||||
.link-preview, .attachments {
|
||||
.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%;
|
||||
|
||||
.status {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
border-radius: $fallback--chatMessageRadius;
|
||||
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
||||
|
|
@ -86,7 +71,7 @@
|
|||
position: relative;
|
||||
float: right;
|
||||
font-size: 0.8em;
|
||||
margin: -1em 0 -0.5em 0;
|
||||
margin: -1em 0 -0.5em;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
|
@ -103,18 +88,54 @@
|
|||
}
|
||||
|
||||
.pending {
|
||||
.status-content.media-body, .created-at {
|
||||
.status-content.media-body,
|
||||
.created-at {
|
||||
color: var(--faint);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
.status-content.media-body, .created-at {
|
||||
.status-content.media-body,
|
||||
.created-at {
|
||||
color: $fallback--cRed;
|
||||
color: var(--badgeNotification, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 80%;
|
||||
min-width: 10em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.outgoing {
|
||||
display: flex;
|
||||
flex-flow: row 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;
|
||||
}
|
||||
}
|
||||
|
||||
.incoming {
|
||||
a {
|
||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||
|
|
@ -137,36 +158,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.outgoing {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: end;
|
||||
justify-content: flex-end;
|
||||
|
||||
a {
|
||||
color: var(--chatMessageOutgoingLink, $fallback--link);
|
||||
}
|
||||
.chat-message-inner.with-media {
|
||||
width: 100%;
|
||||
|
||||
.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;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-message-date-separator {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<div
|
||||
class="media status"
|
||||
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
||||
style="position: relative"
|
||||
style="position: relative;"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
>
|
||||
|
|
@ -98,6 +98,6 @@
|
|||
|
||||
<script src="./chat_message.js"></script>
|
||||
<style lang="scss">
|
||||
@import './chat_message.scss';
|
||||
@import "./chat_message";
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.chat-new {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||
margin: 0.7em 0.5em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,6 @@
|
|||
|
||||
<script src="./chat_new.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import './chat_new.scss';
|
||||
@import "../../variables";
|
||||
@import "./chat_new";
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<script src="./chat_title.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.chat-title {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
|
|
@ -49,13 +49,13 @@ export default {
|
|||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
content: '✓';
|
||||
content: "✓";
|
||||
transition: color 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0px 0px 2px black inset;
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
|
@ -71,15 +71,16 @@ export default {
|
|||
&.disabled {
|
||||
.checkbox-indicator::before,
|
||||
.label {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
|
|
@ -88,15 +89,14 @@ export default {
|
|||
}
|
||||
|
||||
&:indeterminate + .checkbox-indicator::before {
|
||||
content: '–';
|
||||
content: "–";
|
||||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: .5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.color-input {
|
||||
display: inline-flex;
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: .2em 8px;
|
||||
padding: 0.2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
|
|
@ -27,33 +27,39 @@
|
|||
&.nativeColor {
|
||||
flex: 0 0 2em;
|
||||
min-width: 2em;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.computedIndicator,
|
||||
.transparentIndicator {
|
||||
flex: 0 0 2em;
|
||||
min-width: 2em;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.transparentIndicator {
|
||||
// forgot to install counter-strike source, ooops
|
||||
background-color: #FF00FF;
|
||||
background-color: #f0f;
|
||||
position: relative;
|
||||
&::before, &::after {
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #000000;
|
||||
content: "";
|
||||
background-color: #000;
|
||||
position: absolute;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
|
@ -64,5 +70,4 @@
|
|||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ export default {
|
|||
.contrast-ratio {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-top: -4px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||
import Status from '../status/status.vue'
|
||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
|
||||
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -77,6 +81,9 @@ const conversation = {
|
|||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||
return maxDepth >= 1 ? maxDepth : 1
|
||||
},
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
},
|
||||
displayStyle () {
|
||||
return this.$store.getters.mergedConfig.conversationDisplay
|
||||
},
|
||||
|
|
@ -339,11 +346,17 @@ const conversation = {
|
|||
},
|
||||
maybeHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Status,
|
||||
ThreadTree
|
||||
ThreadTree,
|
||||
QuickFilterSettings,
|
||||
QuickViewSettings
|
||||
},
|
||||
watch: {
|
||||
statusId (newVal, oldVal) {
|
||||
|
|
@ -395,6 +408,11 @@ const conversation = {
|
|||
setHighlight (id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
|
||||
if (!this.streamingEnabled) {
|
||||
this.$store.dispatch('fetchStatus', id)
|
||||
}
|
||||
|
||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@
|
|||
>
|
||||
{{ $t('timeline.collapse') }}
|
||||
</button>
|
||||
<QuickFilterSettings
|
||||
v-if="!collapsable"
|
||||
:conversation="true"
|
||||
class="rightside-button"
|
||||
/>
|
||||
<QuickViewSettings
|
||||
v-if="!collapsable"
|
||||
:conversation="true"
|
||||
class="rightside-button"
|
||||
/>
|
||||
</div>
|
||||
<div class="conversation-body panel-body">
|
||||
<div
|
||||
|
|
@ -50,7 +60,7 @@
|
|||
v-if="shouldShowAncestors"
|
||||
class="thread-ancestors"
|
||||
>
|
||||
<div
|
||||
<article
|
||||
v-for="status in ancestorsOf(diveRoot)"
|
||||
:key="status.id"
|
||||
class="thread-ancestor"
|
||||
|
|
@ -120,7 +130,7 @@
|
|||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<thread-tree
|
||||
v-for="status in showingTopLevel"
|
||||
|
|
@ -158,34 +168,36 @@
|
|||
v-if="isLinearView"
|
||||
class="thread-body"
|
||||
>
|
||||
<status
|
||||
v-for="status in conversation"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
<article>
|
||||
<status
|
||||
v-for="status in conversation"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
:total-reply-count="totalReplyCount"
|
||||
:total-reply-depth="totalReplyDepth"
|
||||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
:total-reply-count="totalReplyCount"
|
||||
:total-reply-depth="totalReplyDepth"
|
||||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -198,17 +210,16 @@
|
|||
<script src="./conversation.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Conversation {
|
||||
z-index: 1;
|
||||
|
||||
.conversation-dive-to-top-level-box {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
|
||||
/* Make the button stretch along the whole row */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
|
@ -223,52 +234,48 @@
|
|||
.thread-ancestor.-faded .StatusContent {
|
||||
--link: var(--faintLink);
|
||||
--text: var(--faint);
|
||||
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.thread-ancestor-dive-box {
|
||||
padding-left: var(--status-margin, $status-margin);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
|
||||
/* Make the button stretch along the whole row */
|
||||
&, &-inner {
|
||||
&,
|
||||
&-inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-ancestor-dive-box-inner {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.conversation-status {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.thread-ancestor-has-other-replies .conversation-status,
|
||||
&:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||
&:last-child .conversation-status,
|
||||
&.-expanded .thread-tree .conversation-status {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.thread-ancestors + .thread-tree > .conversation-status {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-color: var(--border, $fallback--border);
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
/* expanded conversation in timeline */
|
||||
&.status-fadein.-expanded .thread-body {
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
border-left-color: $fallback--cRed;
|
||||
border-left: 4px solid $fallback--cRed;
|
||||
border-left-color: var(--cRed, $fallback--cRed);
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.DesktopNav {
|
||||
width: 100%;
|
||||
|
|
@ -23,13 +23,37 @@
|
|||
max-width: 980px;
|
||||
}
|
||||
|
||||
&.-column-stretch .inner-nav {
|
||||
--miniColumn: 25rem;
|
||||
--maxiColumn: 45rem;
|
||||
--columnGap: 1em;
|
||||
|
||||
max-width:
|
||||
calc(
|
||||
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||
var(--columnGap)
|
||||
);
|
||||
}
|
||||
|
||||
&.-logoLeft .inner-nav {
|
||||
grid-template-columns: auto 2fr 2fr;
|
||||
grid-template-areas: "logo sitename actions";
|
||||
}
|
||||
|
||||
&.-column-stretch.-wide .inner-nav {
|
||||
max-width:
|
||||
calc(
|
||||
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||
var(--notifsColumnWidth, var(--miniColumn)) +
|
||||
var(--columnGap)
|
||||
);
|
||||
}
|
||||
|
||||
.button-default {
|
||||
&, svg {
|
||||
&,
|
||||
svg {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
|
@ -50,7 +74,7 @@
|
|||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +86,7 @@
|
|||
transition-duration: 100ms;
|
||||
|
||||
@media all and (min-width: 800px) {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
|
@ -117,4 +142,8 @@
|
|||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
/>
|
||||
<button
|
||||
class="button-unstyled nav-icon"
|
||||
@click="openSettingsModal"
|
||||
@click.stop="openSettingsModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
|
@ -61,10 +61,11 @@
|
|||
:title="$t('nav.administration')"
|
||||
/>
|
||||
</a>
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.prevent="logout"
|
||||
@click.stop.prevent="logout"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<script src="./dialog_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
// TODO: unify with other modals.
|
||||
.dark-overlay {
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgba(27,31,35,.5);
|
||||
background: rgb(27 31 35 / 50%);
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
|
||||
.dialog-modal-content {
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
padding: 1rem;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
white-space: normal;
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
padding: 0.5em;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
|
||||
button {
|
||||
width: auto;
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
src/components/edit_status_modal/edit_status_modal.js
Normal file
75
src/components/edit_status_modal/edit_status_modal.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import Modal from '../modal/modal.vue'
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
import get from 'lodash/get'
|
||||
|
||||
const EditStatusModal = {
|
||||
components: {
|
||||
PostStatusForm,
|
||||
Modal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resettingForm: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.editStatus.modalActivated
|
||||
},
|
||||
isFormVisible () {
|
||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||
},
|
||||
params () {
|
||||
return this.$store.state.editStatus.params || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
params (newVal, oldVal) {
|
||||
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||
this.resettingForm = true
|
||||
this.$nextTick(() => {
|
||||
this.resettingForm = false
|
||||
})
|
||||
}
|
||||
},
|
||||
isFormVisible (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
const params = {
|
||||
store: this.$store,
|
||||
statusId: this.$store.state.editStatus.params.statusId,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
media,
|
||||
contentType
|
||||
}
|
||||
|
||||
return statusPosterService.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error editing status', err)
|
||||
return {
|
||||
error: err.message
|
||||
}
|
||||
})
|
||||
},
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeEditStatusModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EditStatusModal
|
||||
49
src/components/edit_status_modal/edit_status_modal.vue
Normal file
49
src/components/edit_status_modal/edit_status_modal.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<Modal
|
||||
v-if="isFormVisible"
|
||||
class="edit-form-modal-view"
|
||||
@backdropClicked="closeModal"
|
||||
>
|
||||
<div class="edit-form-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
{{ $t('post_status.edit_status') }}
|
||||
</div>
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
:post-handler="doEditStatus"
|
||||
:disable-polls="true"
|
||||
:disable-visibility-selector="true"
|
||||
@posted="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./edit_status_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal-view.edit-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.edit-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
|
||||
.form-bottom-left {
|
||||
max-width: 6.5em;
|
||||
|
||||
.emoji-icon {
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import Completion from '../../services/completion/completion.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSmileBeam
|
||||
|
|
@ -108,46 +110,122 @@ const EmojiInput = {
|
|||
data () {
|
||||
return {
|
||||
input: undefined,
|
||||
caretEl: undefined,
|
||||
highlighted: 0,
|
||||
caret: 0,
|
||||
focused: false,
|
||||
blurTimeout: null,
|
||||
showPicker: false,
|
||||
temporarilyHideSuggestions: false,
|
||||
keepOpen: false,
|
||||
disableClickOutside: false,
|
||||
suggestions: []
|
||||
suggestions: [],
|
||||
overlayStyle: {},
|
||||
pickerShown: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
Popover,
|
||||
EmojiPicker,
|
||||
UnicodeDomainIndicator
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
preText () {
|
||||
return this.modelValue.slice(0, this.caret)
|
||||
},
|
||||
postText () {
|
||||
return this.modelValue.slice(this.caret)
|
||||
},
|
||||
showSuggestions () {
|
||||
return this.focused &&
|
||||
this.suggestions &&
|
||||
this.suggestions.length > 0 &&
|
||||
!this.showPicker &&
|
||||
!this.pickerShown &&
|
||||
!this.temporarilyHideSuggestions
|
||||
},
|
||||
textAtCaret () {
|
||||
return (this.wordAtCaret || {}).word || ''
|
||||
return this.wordAtCaret?.word
|
||||
},
|
||||
wordAtCaret () {
|
||||
if (this.modelValue && this.caret) {
|
||||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
return word
|
||||
}
|
||||
},
|
||||
languages () {
|
||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||
},
|
||||
maybeLocalizedEmojiNamesAndKeywords () {
|
||||
return emoji => {
|
||||
const names = [emoji.displayText]
|
||||
const keywords = []
|
||||
|
||||
if (emoji.displayTextI18n) {
|
||||
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
|
||||
}
|
||||
|
||||
if (emoji.annotations) {
|
||||
this.languages.forEach(lang => {
|
||||
names.push(emoji.annotations[lang]?.name)
|
||||
|
||||
keywords.push(...(emoji.annotations[lang]?.keywords || []))
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
names: names.filter(k => k),
|
||||
keywords: keywords.filter(k => k)
|
||||
}
|
||||
}
|
||||
},
|
||||
maybeLocalizedEmojiName () {
|
||||
return emoji => {
|
||||
if (!emoji.annotations) {
|
||||
return emoji.displayText
|
||||
}
|
||||
|
||||
if (emoji.displayTextI18n) {
|
||||
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
|
||||
}
|
||||
|
||||
for (const lang of this.languages) {
|
||||
if (emoji.annotations[lang]?.name) {
|
||||
return emoji.annotations[lang].name
|
||||
}
|
||||
}
|
||||
|
||||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
onInputScroll () {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
const { root } = this.$refs
|
||||
const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
|
||||
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
||||
if (!input) return
|
||||
this.input = input
|
||||
this.caretEl = hiddenOverlayCaret
|
||||
if (suggestorPopover.setAnchorEl) {
|
||||
suggestorPopover.setAnchorEl(this.caretEl) // unit test compat
|
||||
this.$refs.picker.setAnchorEl(this.caretEl)
|
||||
} else {
|
||||
console.warn('setAnchorEl not found, are we in a unit test?')
|
||||
}
|
||||
const style = getComputedStyle(this.input)
|
||||
this.overlayStyle.padding = style.padding
|
||||
this.overlayStyle.border = style.border
|
||||
this.overlayStyle.margin = style.margin
|
||||
this.overlayStyle.lineHeight = style.lineHeight
|
||||
this.overlayStyle.fontFamily = style.fontFamily
|
||||
this.overlayStyle.fontSize = style.fontSize
|
||||
this.overlayStyle.wordWrap = style.wordWrap
|
||||
this.overlayStyle.whiteSpace = style.whiteSpace
|
||||
this.resize()
|
||||
input.addEventListener('blur', this.onBlur)
|
||||
input.addEventListener('focus', this.onFocus)
|
||||
|
|
@ -157,6 +235,7 @@ const EmojiInput = {
|
|||
input.addEventListener('click', this.onClickInput)
|
||||
input.addEventListener('transitionend', this.onTransition)
|
||||
input.addEventListener('input', this.onInput)
|
||||
input.addEventListener('scroll', this.onInputScroll)
|
||||
},
|
||||
unmounted () {
|
||||
const { input } = this
|
||||
|
|
@ -169,46 +248,43 @@ const EmojiInput = {
|
|||
input.removeEventListener('click', this.onClickInput)
|
||||
input.removeEventListener('transitionend', this.onTransition)
|
||||
input.removeEventListener('input', this.onInput)
|
||||
input.removeEventListener('scroll', this.onInputScroll)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showSuggestions: function (newValue) {
|
||||
showSuggestions: function (newValue, oldValue) {
|
||||
this.$emit('shown', newValue)
|
||||
if (newValue) {
|
||||
this.$refs.suggestorPopover.showPopover()
|
||||
} else {
|
||||
this.$refs.suggestorPopover.hidePopover()
|
||||
}
|
||||
},
|
||||
textAtCaret: async function (newWord) {
|
||||
if (newWord === undefined) return
|
||||
const firstchar = newWord.charAt(0)
|
||||
this.suggestions = []
|
||||
if (newWord === firstchar) return
|
||||
const matchedSuggestions = await this.suggest(newWord)
|
||||
if (newWord === firstchar) {
|
||||
this.suggestions = []
|
||||
return
|
||||
}
|
||||
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
|
||||
// Async: cancel if textAtCaret has changed during wait
|
||||
if (this.textAtCaret !== newWord) return
|
||||
if (matchedSuggestions.length <= 0) return
|
||||
if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
|
||||
this.suggestions = []
|
||||
return
|
||||
}
|
||||
this.suggestions = take(matchedSuggestions, 5)
|
||||
.map(({ imageUrl, ...rest }) => ({
|
||||
...rest,
|
||||
img: imageUrl || ''
|
||||
}))
|
||||
},
|
||||
suggestions: {
|
||||
handler (newValue) {
|
||||
this.$nextTick(this.resize)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
focusPickerInput () {
|
||||
const pickerEl = this.$refs.picker.$el
|
||||
if (!pickerEl) return
|
||||
const pickerInput = pickerEl.querySelector('input')
|
||||
if (pickerInput) pickerInput.focus()
|
||||
},
|
||||
triggerShowPicker () {
|
||||
this.showPicker = true
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.picker.showPicker()
|
||||
this.scrollIntoView()
|
||||
this.focusPickerInput()
|
||||
})
|
||||
// This temporarily disables "click outside" handler
|
||||
// since external trigger also means click originates
|
||||
|
|
@ -220,11 +296,12 @@ const EmojiInput = {
|
|||
},
|
||||
togglePicker () {
|
||||
this.input.focus()
|
||||
this.showPicker = !this.showPicker
|
||||
if (this.showPicker) {
|
||||
if (!this.pickerShown) {
|
||||
this.scrollIntoView()
|
||||
this.$refs.picker.showPicker()
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
this.$nextTick(this.focusPickerInput)
|
||||
} else {
|
||||
this.$refs.picker.hidePicker()
|
||||
}
|
||||
},
|
||||
replace (replacement) {
|
||||
|
|
@ -261,7 +338,6 @@ const EmojiInput = {
|
|||
spaceAfter,
|
||||
after
|
||||
].join('')
|
||||
this.keepOpen = keepOpen
|
||||
this.$emit('update:modelValue', newValue)
|
||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
if (!keepOpen) {
|
||||
|
|
@ -361,8 +437,11 @@ const EmojiInput = {
|
|||
}
|
||||
})
|
||||
},
|
||||
onTransition (e) {
|
||||
this.resize()
|
||||
onPickerShown () {
|
||||
this.pickerShown = true
|
||||
},
|
||||
onPickerClosed () {
|
||||
this.pickerShown = false
|
||||
},
|
||||
onBlur (e) {
|
||||
// Clicking on any suggestion removes focus from autocomplete,
|
||||
|
|
@ -370,7 +449,6 @@ const EmojiInput = {
|
|||
this.blurTimeout = setTimeout(() => {
|
||||
this.focused = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
}, 200)
|
||||
},
|
||||
onClick (e, suggestion) {
|
||||
|
|
@ -382,18 +460,13 @@ const EmojiInput = {
|
|||
this.blurTimeout = null
|
||||
}
|
||||
|
||||
if (!this.keepOpen) {
|
||||
this.showPicker = false
|
||||
}
|
||||
this.focused = true
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
this.temporarilyHideSuggestions = false
|
||||
},
|
||||
onKeyUp (e) {
|
||||
const { key } = e
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
|
||||
// Setting hider in keyUp to prevent suggestions from blinking
|
||||
// when moving away from suggested spot
|
||||
|
|
@ -405,7 +478,6 @@ const EmojiInput = {
|
|||
},
|
||||
onPaste (e) {
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
},
|
||||
onKeyDown (e) {
|
||||
const { ctrlKey, shiftKey, key } = e
|
||||
|
|
@ -450,58 +522,24 @@ const EmojiInput = {
|
|||
this.input.focus()
|
||||
}
|
||||
}
|
||||
|
||||
this.showPicker = false
|
||||
this.resize()
|
||||
},
|
||||
onInput (e) {
|
||||
this.showPicker = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
this.$emit('update:modelValue', e.target.value)
|
||||
},
|
||||
onClickInput (e) {
|
||||
this.showPicker = false
|
||||
},
|
||||
onClickOutside (e) {
|
||||
if (this.disableClickOutside) return
|
||||
this.showPicker = false
|
||||
},
|
||||
onStickerUploaded (e) {
|
||||
this.showPicker = false
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed (e) {
|
||||
this.showPicker = false
|
||||
this.$emit('sticker-upload-Failed', e)
|
||||
},
|
||||
setCaret ({ target: { selectionStart } }) {
|
||||
this.caret = selectionStart
|
||||
this.$nextTick(() => {
|
||||
this.$refs.suggestorPopover.updateStyles()
|
||||
})
|
||||
},
|
||||
resize () {
|
||||
const panel = this.$refs.panel
|
||||
if (!panel) return
|
||||
const picker = this.$refs.picker.$el
|
||||
const panelBody = this.$refs['panel-body']
|
||||
const { offsetHeight, offsetTop } = this.input
|
||||
const offsetBottom = offsetTop + offsetHeight
|
||||
|
||||
this.setPlacement(panelBody, panel, offsetBottom)
|
||||
this.setPlacement(picker, picker, offsetBottom)
|
||||
},
|
||||
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.offsetHeight + 'px'
|
||||
}
|
||||
},
|
||||
overflowsBottom (el) {
|
||||
return el.getBoundingClientRect().bottom > window.innerHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
v-click-outside="onClickOutside"
|
||||
class="emoji-input"
|
||||
:class="{ 'with-picker': !hideEmojiButton }"
|
||||
>
|
||||
<slot />
|
||||
<!-- TODO: make the 'x' disappear if at the end maybe? -->
|
||||
<div
|
||||
ref="hiddenOverlay"
|
||||
class="hidden-overlay"
|
||||
:style="overlayStyle"
|
||||
>
|
||||
<span>{{ preText }}</span>
|
||||
<span
|
||||
ref="hiddenOverlayCaret"
|
||||
class="caret"
|
||||
>x</span>
|
||||
<span>{{ postText }}</span>
|
||||
</div>
|
||||
<template v-if="enableEmojiPicker">
|
||||
<button
|
||||
v-if="!hideEmojiButton"
|
||||
|
|
@ -18,66 +30,79 @@
|
|||
<EmojiPicker
|
||||
v-if="enableEmojiPicker"
|
||||
ref="picker"
|
||||
:class="{ hide: !showPicker }"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
class="emoji-picker-panel"
|
||||
@emoji="insert"
|
||||
@sticker-uploaded="onStickerUploaded"
|
||||
@sticker-upload-failed="onStickerUploadFailed"
|
||||
@show="onPickerShown"
|
||||
@close="onPickerClosed"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
ref="panel"
|
||||
<Popover
|
||||
ref="suggestorPopover"
|
||||
class="autocomplete-panel"
|
||||
:class="{ hide: !showSuggestions }"
|
||||
placement="bottom"
|
||||
>
|
||||
<div
|
||||
ref="panel-body"
|
||||
class="autocomplete-panel-body"
|
||||
>
|
||||
<template #content>
|
||||
<div
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="index"
|
||||
class="autocomplete-item"
|
||||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
ref="panel-body"
|
||||
class="autocomplete-panel-body"
|
||||
>
|
||||
<span class="image">
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
<span class="displayText">{{ suggestion.displayText }}</span>
|
||||
<span class="detailText">{{ suggestion.detailText }}</span>
|
||||
<div
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="index"
|
||||
class="autocomplete-item"
|
||||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span class="image">
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
<span
|
||||
v-if="suggestion.user"
|
||||
class="displayText"
|
||||
>
|
||||
{{ suggestion.displayText }}<UnicodeDomainIndicator
|
||||
:user="suggestion.user"
|
||||
:at="false"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="!suggestion.user"
|
||||
class="displayText"
|
||||
>
|
||||
{{ maybeLocalizedEmojiName(suggestion) }}
|
||||
</span>
|
||||
<span class="detailText">{{ suggestion.detailText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_input.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.emoji-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.emoji-picker-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: .2em .25em;
|
||||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
|
|
@ -87,99 +112,102 @@
|
|||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-picker-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
&-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
input,
|
||||
textarea {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
}
|
||||
&.with-picker input {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
&-body {
|
||||
margin: 0 0.5em 0 0.5em;
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
min-width: 75%;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--postLink: var(--popoverPostLink, $fallback--link);
|
||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
}
|
||||
.hidden-overlay {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* DEBUG STUFF */
|
||||
color: red;
|
||||
|
||||
/* set opacity to non-zero to see the overlay */
|
||||
|
||||
.caret {
|
||||
width: 0;
|
||||
margin-right: calc(-1ch - 1px);
|
||||
border: 1px solid red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 0.2em 0.4em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
|
||||
.autocomplete {
|
||||
&-panel {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 0.2em 0.4em;
|
||||
border-bottom: 1px solid rgb(0 0 0 / 40%);
|
||||
height: 32px;
|
||||
|
||||
.image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
margin-right: 4px;
|
||||
|
||||
.image {
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
|
||||
margin-right: 4px;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0 0.1em 0 0.2em;
|
||||
|
||||
.displayText {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detailText {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--selectedMenuPopover, $fallback--fg);
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
flex: 1 0 auto;
|
||||
.label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0 0.1em 0 0.2em;
|
||||
|
||||
.displayText {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detailText {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--selectedMenuPopover, $fallback--fg);
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
* data.emoji - optional, an array of all emoji available i.e.
|
||||
* (state.instance.emoji + state.instance.customEmoji)
|
||||
* (getters.standardEmojiList + state.instance.customEmoji)
|
||||
* data.users - optional, an array of all known users
|
||||
* updateUsersList - optional, a function to search and append to users
|
||||
*
|
||||
|
|
@ -13,10 +13,10 @@
|
|||
export default data => {
|
||||
const emojiCurry = suggestEmoji(data.emoji)
|
||||
const usersCurry = data.store && suggestUsers(data.store)
|
||||
return input => {
|
||||
return (input, nameKeywordLocalizer) => {
|
||||
const firstChar = input[0]
|
||||
if (firstChar === ':' && data.emoji) {
|
||||
return emojiCurry(input)
|
||||
return emojiCurry(input, nameKeywordLocalizer)
|
||||
}
|
||||
if (firstChar === '@' && usersCurry) {
|
||||
return usersCurry(input)
|
||||
|
|
@ -25,34 +25,34 @@ export default data => {
|
|||
}
|
||||
}
|
||||
|
||||
export const suggestEmoji = emojis => input => {
|
||||
export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
.map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||
.filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
|
||||
.map(k => {
|
||||
let score = 0
|
||||
|
||||
// An exact match always wins
|
||||
aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
|
||||
|
||||
// Prioritize custom emoji a lot
|
||||
aScore += a.imageUrl ? 100 : 0
|
||||
bScore += b.imageUrl ? 100 : 0
|
||||
score += k.imageUrl ? 100 : 0
|
||||
|
||||
// Prioritize prefix matches somewhat
|
||||
aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
|
||||
|
||||
// Sort by length
|
||||
aScore -= a.displayText.length
|
||||
bScore -= b.displayText.length
|
||||
score -= k.displayText.length
|
||||
|
||||
k.score = score
|
||||
return k
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Break ties alphabetically
|
||||
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
||||
|
||||
return bScore - aScore + alphabetically
|
||||
return b.score - a.score + alphabetically
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}).map((user) => ({
|
||||
user,
|
||||
displayText: user.screen_name_ui,
|
||||
detailText: user.name,
|
||||
imageUrl: user.profile_image_url_original,
|
||||
replacement: '@' + user.screen_name + ' '
|
||||
}))
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,76 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam
|
||||
faSmileBeam,
|
||||
faSmile,
|
||||
faUser,
|
||||
faPaw,
|
||||
faIceCream,
|
||||
faBus,
|
||||
faBasketballBall,
|
||||
faLightbulb,
|
||||
faCode,
|
||||
faFlag
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { trim } from 'lodash'
|
||||
import { debounce, trim, chunk } from 'lodash'
|
||||
|
||||
library.add(
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam
|
||||
faSmileBeam,
|
||||
faSmile,
|
||||
faUser,
|
||||
faPaw,
|
||||
faIceCream,
|
||||
faBus,
|
||||
faBasketballBall,
|
||||
faLightbulb,
|
||||
faCode,
|
||||
faFlag
|
||||
)
|
||||
|
||||
// At widest, approximately 20 emoji are visible in a row,
|
||||
// loading 3 rows, could be overkill for narrow picker
|
||||
const LOAD_EMOJI_BY = 60
|
||||
const UNICODE_EMOJI_GROUP_ICON = {
|
||||
'smileys-and-emotion': 'smile',
|
||||
'people-and-body': 'user',
|
||||
'animals-and-nature': 'paw',
|
||||
'food-and-drink': 'ice-cream',
|
||||
'travel-and-places': 'bus',
|
||||
activities: 'basketball-ball',
|
||||
objects: 'lightbulb',
|
||||
symbols: 'code',
|
||||
flags: 'flag'
|
||||
}
|
||||
|
||||
// When to start loading new batch emoji, in pixels
|
||||
const LOAD_EMOJI_MARGIN = 64
|
||||
const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
|
||||
const res = [emoji.displayText, nameLocalizer(emoji)]
|
||||
if (emoji.annotations) {
|
||||
languages.forEach(lang => {
|
||||
const keywords = emoji.annotations[lang]?.keywords || []
|
||||
const name = emoji.annotations[lang]?.name
|
||||
res.push(...(keywords.concat([name]).filter(k => k)))
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const filterByKeyword = (list, keyword = '') => {
|
||||
const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
|
||||
if (keyword === '') return list
|
||||
|
||||
const keywordLowercase = keyword.toLowerCase()
|
||||
const orderedEmojiList = []
|
||||
for (const emoji of list) {
|
||||
const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
|
||||
const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
|
||||
.map(k => k.toLowerCase().indexOf(keywordLowercase))
|
||||
.filter(k => k > -1)
|
||||
|
||||
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
|
||||
|
||||
if (indexOfKeyword > -1) {
|
||||
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||
orderedEmojiList[indexOfKeyword] = []
|
||||
|
|
@ -38,6 +81,17 @@ const filterByKeyword = (list, keyword = '') => {
|
|||
return orderedEmojiList.flat()
|
||||
}
|
||||
|
||||
const getOffset = (elem) => {
|
||||
const style = elem.style.transform
|
||||
const res = /translateY\((\d+)px\)/.exec(style)
|
||||
if (!res) { return 0 }
|
||||
return res[1]
|
||||
}
|
||||
|
||||
const toHeaderId = id => {
|
||||
return id.replace(/^row-\d+-/, '')
|
||||
}
|
||||
|
||||
const EmojiPicker = {
|
||||
props: {
|
||||
enableStickerPicker: {
|
||||
|
|
@ -53,16 +107,41 @@ const EmojiPicker = {
|
|||
showingStickers: false,
|
||||
groupsScrolledClass: 'scrolled-top',
|
||||
keepOpen: false,
|
||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
||||
customEmojiTimeout: null,
|
||||
customEmojiLoadAllConfirmed: false
|
||||
// Lazy-load only after the first time `showing` becomes true.
|
||||
contentLoaded: false,
|
||||
groupRefs: {},
|
||||
emojiRefs: {},
|
||||
filteredEmojiGroups: [],
|
||||
width: 0
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
Checkbox
|
||||
Checkbox,
|
||||
StillImage,
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
showPicker () {
|
||||
this.$refs.popover.showPopover()
|
||||
this.onShowing()
|
||||
},
|
||||
hidePicker () {
|
||||
this.$refs.popover.hidePopover()
|
||||
},
|
||||
setAnchorEl (el) {
|
||||
this.$refs.popover.setAnchorEl(el)
|
||||
},
|
||||
setGroupRef (name) {
|
||||
return el => { this.groupRefs[name] = el }
|
||||
},
|
||||
onPopoverShown () {
|
||||
this.$emit('show')
|
||||
},
|
||||
onPopoverClosed () {
|
||||
this.$emit('close')
|
||||
},
|
||||
onStickerUploaded (e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
|
|
@ -71,23 +150,53 @@ const EmojiPicker = {
|
|||
},
|
||||
onEmoji (emoji) {
|
||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||
if (!this.keepOpen) {
|
||||
this.$refs.popover.hidePopover()
|
||||
}
|
||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||
},
|
||||
onScroll (e) {
|
||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||
this.updateScrolledClass(target)
|
||||
this.scrolledGroup(target)
|
||||
this.triggerLoadMore(target)
|
||||
onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
|
||||
const target = this.$refs['emoji-groups'].$el
|
||||
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
|
||||
},
|
||||
highlight (key) {
|
||||
const ref = this.$refs['group-' + key]
|
||||
const top = ref.offsetTop
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
scrolledGroup (target, start, end) {
|
||||
const top = target.scrollTop + 5
|
||||
this.$nextTick(() => {
|
||||
this.$refs['emoji-groups'].scrollTop = top + 1
|
||||
this.emojiItems.slice(start, end + 1).forEach(group => {
|
||||
const headerId = toHeaderId(group.id)
|
||||
const ref = this.groupRefs['group-' + group.id]
|
||||
if (!ref) { return }
|
||||
const elem = ref.$el.parentElement
|
||||
if (!elem) { return }
|
||||
if (elem && getOffset(elem) <= top) {
|
||||
this.activeGroup = headerId
|
||||
}
|
||||
})
|
||||
this.scrollHeader()
|
||||
})
|
||||
},
|
||||
scrollHeader () {
|
||||
// Scroll the active tab's header into view
|
||||
const headerRef = this.groupRefs['group-header-' + this.activeGroup]
|
||||
const left = headerRef.offsetLeft
|
||||
const right = left + headerRef.offsetWidth
|
||||
const headerCont = this.$refs.header
|
||||
const currentScroll = headerCont.scrollLeft
|
||||
const currentScrollRight = currentScroll + headerCont.clientWidth
|
||||
const setScroll = s => { headerCont.scrollLeft = s }
|
||||
|
||||
const margin = 7 // .emoji-tabs-item: padding
|
||||
if (left - margin < currentScroll) {
|
||||
setScroll(left - margin)
|
||||
} else if (right + margin > currentScrollRight) {
|
||||
setScroll(right + margin - headerCont.clientWidth)
|
||||
}
|
||||
},
|
||||
highlight (groupId) {
|
||||
this.setShowStickers(false)
|
||||
const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
|
||||
this.$refs['emoji-groups'].scrollToItem(indexInList)
|
||||
},
|
||||
updateScrolledClass (target) {
|
||||
if (target.scrollTop <= 5) {
|
||||
this.groupsScrolledClass = 'scrolled-top'
|
||||
|
|
@ -97,74 +206,70 @@ const EmojiPicker = {
|
|||
this.groupsScrolledClass = 'scrolled-middle'
|
||||
}
|
||||
},
|
||||
triggerLoadMore (target) {
|
||||
const ref = this.$refs['group-end-custom']
|
||||
if (!ref) return
|
||||
const bottom = ref.offsetTop + ref.offsetHeight
|
||||
|
||||
const scrollerBottom = target.scrollTop + target.clientHeight
|
||||
const scrollerTop = target.scrollTop
|
||||
const scrollerMax = target.scrollHeight
|
||||
|
||||
// Loads more emoji when they come into view
|
||||
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
|
||||
// Always load when at the very top in case there's no scroll space yet
|
||||
const atTop = scrollerTop < 5
|
||||
// Don't load when looking at unicode category or at the very bottom
|
||||
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
|
||||
if (!bottomAboveViewport && (approachingBottom || atTop)) {
|
||||
this.loadEmoji()
|
||||
}
|
||||
},
|
||||
scrolledGroup (target) {
|
||||
const top = target.scrollTop + 5
|
||||
this.$nextTick(() => {
|
||||
this.emojisView.forEach(group => {
|
||||
const ref = this.$refs['group-' + group.id]
|
||||
if (ref.offsetTop <= top) {
|
||||
this.activeGroup = group.id
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
loadEmoji () {
|
||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
||||
|
||||
if (allLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
this.customEmojiBufferSlice += LOAD_EMOJI_BY
|
||||
},
|
||||
startEmojiLoad (forceUpdate = false) {
|
||||
if (!forceUpdate) {
|
||||
this.keyword = ''
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs['emoji-groups'].scrollTop = 0
|
||||
})
|
||||
const bufferSize = this.customEmojiBuffer.length
|
||||
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
|
||||
if (bufferPrefilledAll && !forceUpdate) {
|
||||
return
|
||||
}
|
||||
this.customEmojiBufferSlice = LOAD_EMOJI_BY
|
||||
},
|
||||
toggleStickers () {
|
||||
this.showingStickers = !this.showingStickers
|
||||
},
|
||||
setShowStickers (value) {
|
||||
this.showingStickers = value
|
||||
},
|
||||
filterByKeyword (list, keyword) {
|
||||
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
|
||||
},
|
||||
onShowing () {
|
||||
const oldContentLoaded = this.contentLoaded
|
||||
this.recalculateItemPerRow()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus()
|
||||
})
|
||||
this.contentLoaded = true
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
if (!oldContentLoaded) {
|
||||
this.$nextTick(() => {
|
||||
if (this.defaultGroup) {
|
||||
this.highlight(this.defaultGroup)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
getFilteredEmojiGroups () {
|
||||
return this.allEmojiGroups
|
||||
.map(group => ({
|
||||
...group,
|
||||
emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
|
||||
}))
|
||||
.filter(group => group.emojis.length > 0)
|
||||
},
|
||||
recalculateItemPerRow () {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs['emoji-groups']) {
|
||||
return
|
||||
}
|
||||
this.width = this.$refs['emoji-groups'].$el.clientWidth
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyword () {
|
||||
this.customEmojiLoadAllConfirmed = false
|
||||
this.onScroll()
|
||||
this.startEmojiLoad(true)
|
||||
this.debouncedHandleKeywordChange()
|
||||
},
|
||||
allCustomGroups () {
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
minItemSize () {
|
||||
return this.emojiHeight
|
||||
},
|
||||
emojiHeight () {
|
||||
return 32 + 4
|
||||
},
|
||||
emojiWidth () {
|
||||
return 32 + 4
|
||||
},
|
||||
itemPerRow () {
|
||||
return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6
|
||||
},
|
||||
activeGroupView () {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
},
|
||||
|
|
@ -174,39 +279,69 @@ const EmojiPicker = {
|
|||
}
|
||||
return 0
|
||||
},
|
||||
filteredEmoji () {
|
||||
return filterByKeyword(
|
||||
this.$store.state.instance.customEmoji || [],
|
||||
trim(this.keyword)
|
||||
)
|
||||
allCustomGroups () {
|
||||
const emojis = this.$store.getters.groupedCustomEmojis
|
||||
if (emojis.unpacked) {
|
||||
emojis.unpacked.text = this.$t('emoji.unpacked')
|
||||
}
|
||||
return emojis
|
||||
},
|
||||
customEmojiBuffer () {
|
||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
||||
defaultGroup () {
|
||||
return Object.keys(this.allCustomGroups)[0]
|
||||
},
|
||||
emojis () {
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.customEmojiBuffer
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'custom',
|
||||
text: this.$t('emoji.custom'),
|
||||
icon: 'smile-beam',
|
||||
emojis: customEmojis
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
text: this.$t('emoji.unicode'),
|
||||
icon: 'box-open',
|
||||
emojis: filterByKeyword(standardEmojis, trim(this.keyword))
|
||||
}
|
||||
]
|
||||
unicodeEmojiGroups () {
|
||||
return this.$store.getters.standardEmojiGroupList.map(group => ({
|
||||
id: `standard-${group.id}`,
|
||||
text: this.$t(`emoji.unicode_groups.${group.id}`),
|
||||
icon: UNICODE_EMOJI_GROUP_ICON[group.id],
|
||||
emojis: group.emojis
|
||||
}))
|
||||
},
|
||||
emojisView () {
|
||||
return this.emojis.filter(value => value.emojis.length > 0)
|
||||
allEmojiGroups () {
|
||||
return Object.entries(this.allCustomGroups)
|
||||
.map(([_, v]) => v)
|
||||
.concat(this.unicodeEmojiGroups)
|
||||
},
|
||||
stickerPickerEnabled () {
|
||||
return (this.$store.state.instance.stickers || []).length !== 0
|
||||
},
|
||||
debouncedHandleKeywordChange () {
|
||||
return debounce(() => {
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
}, 500)
|
||||
},
|
||||
emojiItems () {
|
||||
return this.filteredEmojiGroups.map(group =>
|
||||
chunk(group.emojis, this.itemPerRow)
|
||||
.map((items, index) => ({
|
||||
...group,
|
||||
id: index === 0 ? group.id : `row-${index}-${group.id}`,
|
||||
emojis: items,
|
||||
isFirstRow: index === 0
|
||||
})))
|
||||
.reduce((a, c) => a.concat(c), [])
|
||||
},
|
||||
languages () {
|
||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||
},
|
||||
maybeLocalizedEmojiName () {
|
||||
return emoji => {
|
||||
if (!emoji.annotations) {
|
||||
return emoji.displayText
|
||||
}
|
||||
|
||||
if (emoji.displayTextI18n) {
|
||||
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
|
||||
}
|
||||
|
||||
for (const lang of this.languages) {
|
||||
if (emoji.annotations[lang]?.name) {
|
||||
return emoji.annotations[lang].name
|
||||
}
|
||||
}
|
||||
|
||||
return emoji.displayText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,43 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
$emoji-picker-header-height: 36px;
|
||||
$emoji-picker-header-picture-width: 32px;
|
||||
$emoji-picker-header-picture-height: 32px;
|
||||
$emoji-picker-emoji-size: 32px;
|
||||
|
||||
.emoji-picker {
|
||||
width: 25em;
|
||||
max-width: calc(100vw - 20px); // popover gives 10px margin from window edge
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
margin: 0 !important;
|
||||
// TODO: actually use popover in emoji picker
|
||||
z-index: var(--ZI_popovers);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--lightText: var(--popoverLightText, $fallback--faint);
|
||||
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
&-header-image {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: $emoji-picker-header-picture-width;
|
||||
max-width: $emoji-picker-header-picture-width;
|
||||
height: $emoji-picker-header-picture-height;
|
||||
max-height: $emoji-picker-header-picture-height;
|
||||
|
||||
.still-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.keep-open,
|
||||
.too-many-emoji {
|
||||
padding: 7px;
|
||||
|
|
@ -37,7 +56,6 @@
|
|||
|
||||
.heading {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 10px 7px 5px;
|
||||
}
|
||||
|
||||
|
|
@ -45,18 +63,18 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.emoji-tabs {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.emoji-groups {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.additional-tabs {
|
||||
display: flex;
|
||||
border-left: 1px solid;
|
||||
border-left-color: $fallback--icon;
|
||||
border-left-color: var(--icon, $fallback--icon);
|
||||
|
|
@ -66,15 +84,20 @@
|
|||
|
||||
.additional-tabs,
|
||||
.emoji-tabs {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
flex-basis: auto;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
|
||||
&-item {
|
||||
padding: 0 7px;
|
||||
cursor: pointer;
|
||||
font-size: 1.85em;
|
||||
width: $emoji-picker-header-picture-width;
|
||||
max-width: $emoji-picker-header-picture-width;
|
||||
height: $emoji-picker-header-picture-height;
|
||||
max-height: $emoji-picker-header-picture-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
|
|
@ -93,7 +116,7 @@
|
|||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 auto
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.stickers,
|
||||
|
|
@ -123,22 +146,27 @@
|
|||
}
|
||||
|
||||
&-groups {
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
flex: 1 1 1px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
user-select: none;
|
||||
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
mask:
|
||||
linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
|
||||
&.scrolled {
|
||||
&-top {
|
||||
mask-size: 100% 20px, 100% 0, auto;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
mask-size: 100% 0, 100% 20px, auto;
|
||||
}
|
||||
|
|
@ -164,24 +192,26 @@
|
|||
}
|
||||
|
||||
&-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: $emoji-picker-emoji-size;
|
||||
height: $emoji-picker-emoji-size;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
font-size: 32px;
|
||||
line-height: $emoji-picker-emoji-size;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
.emoji-picker-emoji.-custom {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.emoji-picker-emoji.-unicode {
|
||||
font-size: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,105 +1,148 @@
|
|||
<template>
|
||||
<div class="emoji-picker panel panel-default panel-body">
|
||||
<div class="heading">
|
||||
<span class="emoji-tabs">
|
||||
<Popover
|
||||
ref="popover"
|
||||
trigger="click"
|
||||
popover-class="emoji-picker popover-default"
|
||||
@show="onPopoverShown"
|
||||
@close="onPopoverClosed"
|
||||
>
|
||||
<template #content>
|
||||
<div class="heading">
|
||||
<span
|
||||
v-for="group in emojis"
|
||||
:key="group.id"
|
||||
class="emoji-tabs-item"
|
||||
:class="{
|
||||
active: activeGroupView === group.id,
|
||||
disabled: group.emojis.length === 0
|
||||
}"
|
||||
:title="group.text"
|
||||
@click.prevent="highlight(group.id)"
|
||||
ref="header"
|
||||
class="emoji-tabs"
|
||||
>
|
||||
<FAIcon
|
||||
:icon="group.icon"
|
||||
fixed-width
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="stickerPickerEnabled"
|
||||
class="additional-tabs"
|
||||
>
|
||||
<span
|
||||
class="stickers-tab-icon additional-tabs-item"
|
||||
:class="{active: showingStickers}"
|
||||
:title="$t('emoji.stickers')"
|
||||
@click.prevent="toggleStickers"
|
||||
>
|
||||
<FAIcon
|
||||
icon="sticky-note"
|
||||
fixed-width
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
class="emoji-content"
|
||||
:class="{hidden: showingStickers}"
|
||||
>
|
||||
<div class="emoji-search">
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
ref="emoji-groups"
|
||||
class="emoji-groups"
|
||||
:class="groupsScrolledClass"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<div
|
||||
v-for="group in emojisView"
|
||||
<span
|
||||
v-for="group in filteredEmojiGroups"
|
||||
:ref="setGroupRef('group-header-' + group.id)"
|
||||
:key="group.id"
|
||||
class="emoji-group"
|
||||
class="emoji-tabs-item"
|
||||
:class="{
|
||||
active: activeGroupView === group.id
|
||||
}"
|
||||
:title="group.text"
|
||||
@click.prevent="highlight(group.id)"
|
||||
>
|
||||
<h6
|
||||
:ref="'group-' + group.id"
|
||||
class="emoji-group-title"
|
||||
>
|
||||
{{ group.text }}
|
||||
</h6>
|
||||
<span
|
||||
v-for="emoji in group.emojis"
|
||||
:key="group.id + emoji.displayText"
|
||||
:title="emoji.displayText"
|
||||
class="emoji-item"
|
||||
@click.stop.prevent="onEmoji(emoji)"
|
||||
v-if="group.image"
|
||||
class="emoji-picker-header-image"
|
||||
>
|
||||
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
||||
<img
|
||||
v-else
|
||||
:src="emoji.imageUrl"
|
||||
>
|
||||
<still-image
|
||||
:alt="group.text"
|
||||
:src="group.image"
|
||||
/>
|
||||
</span>
|
||||
<span :ref="'group-end-' + group.id" />
|
||||
<FAIcon
|
||||
v-else
|
||||
:icon="group.icon"
|
||||
fixed-width
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="stickerPickerEnabled"
|
||||
class="additional-tabs"
|
||||
>
|
||||
<span
|
||||
class="stickers-tab-icon additional-tabs-item"
|
||||
:class="{active: showingStickers}"
|
||||
:title="$t('emoji.stickers')"
|
||||
@click.prevent="toggleStickers"
|
||||
>
|
||||
<FAIcon
|
||||
icon="sticky-note"
|
||||
fixed-width
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="contentLoaded"
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="emoji-content"
|
||||
:class="{hidden: showingStickers}"
|
||||
>
|
||||
<div class="emoji-search">
|
||||
<input
|
||||
ref="search"
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
>
|
||||
</div>
|
||||
<DynamicScroller
|
||||
ref="emoji-groups"
|
||||
class="emoji-groups"
|
||||
:class="groupsScrolledClass"
|
||||
:min-item-size="minItemSize"
|
||||
:items="emojiItems"
|
||||
:emit-update="true"
|
||||
@update="onScroll"
|
||||
@visible="recalculateItemPerRow"
|
||||
@resize="recalculateItemPerRow"
|
||||
>
|
||||
<template #default="{ item: group, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:ref="setGroupRef('group-' + group.id)"
|
||||
:item="group"
|
||||
:active="active"
|
||||
:data-index="index"
|
||||
:size-dependencies="[group.emojis.length]"
|
||||
>
|
||||
<div
|
||||
class="emoji-group"
|
||||
>
|
||||
<h6
|
||||
v-if="group.isFirstRow"
|
||||
class="emoji-group-title"
|
||||
>
|
||||
{{ group.text }}
|
||||
</h6>
|
||||
<span
|
||||
v-for="emoji in group.emojis"
|
||||
:key="group.id + emoji.displayText"
|
||||
:title="maybeLocalizedEmojiName(emoji)"
|
||||
class="emoji-item"
|
||||
@click.stop.prevent="onEmoji(emoji)"
|
||||
>
|
||||
<span
|
||||
v-if="!emoji.imageUrl"
|
||||
class="emoji-picker-emoji -unicode"
|
||||
>{{ emoji.replacement }}</span>
|
||||
<still-image
|
||||
v-else
|
||||
class="emoji-picker-emoji -custom"
|
||||
loading="lazy"
|
||||
:src="emoji.imageUrl"
|
||||
:data-emoji-name="group.id + emoji.displayText"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
<div class="keep-open">
|
||||
<Checkbox v-model="keepOpen">
|
||||
{{ $t('emoji.keep_open') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="keep-open">
|
||||
<Checkbox v-model="keepOpen">
|
||||
{{ $t('emoji.keep_open') }}
|
||||
</Checkbox>
|
||||
<div
|
||||
v-if="showingStickers"
|
||||
class="stickers-content"
|
||||
>
|
||||
<sticker-picker
|
||||
@uploaded="onStickerUploaded"
|
||||
@upload-failed="onStickerUploadFailed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showingStickers"
|
||||
class="stickers-content"
|
||||
>
|
||||
<sticker-picker
|
||||
@uploaded="onStickerUploaded"
|
||||
@upload-failed="onStickerUploadFailed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_picker.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<div class="EmojiReactions">
|
||||
<UserListPopover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.name"
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
>
|
||||
<button
|
||||
class="emoji-reaction btn button-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
:class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
|
|
@ -28,55 +28,58 @@
|
|||
|
||||
<script src="./emoji_reactions.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.emoji-reactions {
|
||||
.EmojiReactions {
|
||||
display: flex;
|
||||
margin-top: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
.reaction-emoji {
|
||||
width: 1.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
.reaction-emoji {
|
||||
width: 1.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.not-clickable {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $fallback--buttonShadow;
|
||||
box-shadow: var(--buttonShadow);
|
||||
}
|
||||
}
|
||||
|
||||
&.-picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.not-clickable {
|
||||
cursor: default;
|
||||
.emoji-reaction-expand {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $fallback--buttonShadow;
|
||||
box-shadow: var(--buttonShadow);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-reaction-expand {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import {
|
|||
faEyeSlash,
|
||||
faThumbtack,
|
||||
faShareAlt,
|
||||
faExternalLinkAlt
|
||||
faExternalLinkAlt,
|
||||
faHistory,
|
||||
faPlus,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faBookmark as faBookmarkReg,
|
||||
|
|
@ -21,13 +24,27 @@ library.add(
|
|||
faThumbtack,
|
||||
faShareAlt,
|
||||
faExternalLinkAlt,
|
||||
faFlag
|
||||
faFlag,
|
||||
faHistory,
|
||||
faPlus,
|
||||
faTimes
|
||||
)
|
||||
|
||||
const ExtraButtons = {
|
||||
props: ['status'],
|
||||
components: { Popover },
|
||||
data () {
|
||||
return {
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onShow () {
|
||||
this.expanded = true
|
||||
},
|
||||
onClose () {
|
||||
this.expanded = false
|
||||
},
|
||||
deleteStatus () {
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
if (confirmed) {
|
||||
|
|
@ -71,14 +88,32 @@ const ExtraButtons = {
|
|||
},
|
||||
reportStatus () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
||||
},
|
||||
editStatus () {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => this.$store.dispatch('openEditStatusModal', {
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
},
|
||||
showStatusHistory () {
|
||||
const originalStatus = { ...this.status }
|
||||
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
|
||||
stripFieldsList.forEach(p => delete originalStatus[p])
|
||||
this.$store.dispatch('openStatusHistoryModal', originalStatus)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
canDelete () {
|
||||
if (!this.currentUser) { return }
|
||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
return superuser || this.status.user.id === this.currentUser.id
|
||||
return this.currentUser.privileges.includes('messages_delete') || this.status.user.id === this.currentUser.id
|
||||
},
|
||||
ownStatus () {
|
||||
return this.status.user.id === this.currentUser.id
|
||||
|
|
@ -94,7 +129,11 @@ const ExtraButtons = {
|
|||
},
|
||||
statusLink () {
|
||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||
}
|
||||
},
|
||||
isEdited () {
|
||||
return this.status.edited_at !== null
|
||||
},
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
:offset="{ y: 5 }"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
@show="onShow"
|
||||
@close="onClose"
|
||||
>
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
|
|
@ -75,6 +77,28 @@
|
|||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-if="ownStatus && editingAvailable"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="editStatus"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="pen"
|
||||
/><span>{{ $t("status.edit") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isEdited && editingAvailable"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="showStatusHistory"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="history"
|
||||
/><span>{{ $t("status.status_history") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
|
@ -122,10 +146,24 @@
|
|||
</template>
|
||||
<template #trigger>
|
||||
<span class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
<FALayers class="fa-old-padding-layer">
|
||||
<FAIcon
|
||||
class="fa-scale-110 "
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
<FAIcon
|
||||
v-show="!expanded"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-8 right-16"
|
||||
icon="plus"
|
||||
/>
|
||||
<FAIcon
|
||||
v-show="expanded"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-8 right-16"
|
||||
icon="times"
|
||||
/>
|
||||
</FALayers>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
|
|
@ -134,14 +172,10 @@
|
|||
<script src="./extra_buttons.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
.ExtraButtons {
|
||||
/* override of popover internal stuff */
|
||||
.popover-trigger-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.popover-trigger {
|
||||
position: static;
|
||||
padding: 10px;
|
||||
|
|
@ -152,5 +186,22 @@
|
|||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-trigger-button {
|
||||
/* override of popover internal stuff */
|
||||
width: auto;
|
||||
|
||||
@include unfocused-style {
|
||||
.focus-marker {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@include focused-style {
|
||||
.focus-marker {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faStar,
|
||||
faPlus,
|
||||
faMinus,
|
||||
faCheck
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faStar as faStarRegular
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faStar,
|
||||
faStarRegular
|
||||
faStarRegular,
|
||||
faPlus,
|
||||
faMinus,
|
||||
faCheck
|
||||
)
|
||||
|
||||
const FavoriteButton = {
|
||||
|
|
@ -31,7 +39,10 @@ const FavoriteButton = {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['mergedConfig'])
|
||||
...mapGetters(['mergedConfig']),
|
||||
remoteInteractionLink () {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,19 +7,45 @@
|
|||
:title="$t('tool_tip.favorite')"
|
||||
@click.prevent="favorite()"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:icon="[status.favorited ? 'fas' : 'far', 'star']"
|
||||
:spin="animated"
|
||||
/>
|
||||
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||
<FAIcon
|
||||
class="fa-scale-110"
|
||||
:icon="[status.favorited ? 'fas' : 'far', 'star']"
|
||||
:spin="animated"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.favorited"
|
||||
class="active-marker"
|
||||
transform="shrink-6 up-9 right-12"
|
||||
icon="check"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="!status.favorited"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-12"
|
||||
icon="plus"
|
||||
/>
|
||||
<FAIcon
|
||||
v-else
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-12"
|
||||
icon="minus"
|
||||
/>
|
||||
</FALayers>
|
||||
</button>
|
||||
<span v-else>
|
||||
<a
|
||||
v-else
|
||||
class="button-unstyled interactive"
|
||||
target="_blank"
|
||||
role="button"
|
||||
:href="remoteInteractionLink"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:title="$t('tool_tip.favorite')"
|
||||
:icon="['far', 'star']"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<span
|
||||
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
||||
class="action-counter"
|
||||
|
|
@ -32,7 +58,8 @@
|
|||
<script src="./favorite_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
@import "../../mixins";
|
||||
|
||||
.FavoriteButton {
|
||||
display: flex;
|
||||
|
|
@ -57,6 +84,26 @@
|
|||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
|
||||
@include unfocused-style {
|
||||
.focus-marker {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.active-marker {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@include focused-style {
|
||||
.focus-marker {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.active-marker {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@
|
|||
<script src="./flash.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Flash {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
|
||||
.hidden {
|
||||
display: none;
|
||||
visibility: 'hidden';
|
||||
visibility: "hidden";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import FollowButton from '../follow_button/follow_button.vue'
|
||||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||
|
||||
const FollowCard = {
|
||||
props: [
|
||||
|
|
@ -10,7 +11,8 @@ const FollowCard = {
|
|||
components: {
|
||||
BasicUserCard,
|
||||
RemoteFollow,
|
||||
FollowButton
|
||||
FollowButton,
|
||||
RemoveFollowerButton
|
||||
},
|
||||
computed: {
|
||||
isMe () {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@
|
|||
class="follow-card-follow-button"
|
||||
:user="user"
|
||||
/>
|
||||
<RemoveFollowerButton
|
||||
v-if="noFollowsYou && relationship.followed_by"
|
||||
:relationship="relationship"
|
||||
class="follow-card-button"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
|
|
@ -34,12 +39,17 @@
|
|||
&-content-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
&-button {
|
||||
margin-top: 0.5em;
|
||||
padding: 0 1.5em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&-follow-button {
|
||||
margin-top: 0.5em;
|
||||
margin-left: auto;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
<style lang="scss">
|
||||
.follow-request-card-content-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex-flow: row wrap;
|
||||
|
||||
button {
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
|
|
|||
|
|
@ -50,17 +50,20 @@
|
|||
<script src="./font_control.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.font-control {
|
||||
input.custom-font {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
&.custom {
|
||||
/* TODO Should make proper joiners... */
|
||||
.font-switcher {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.custom-font {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { sumBy, set } from 'lodash'
|
|||
const Gallery = {
|
||||
props: [
|
||||
'attachments',
|
||||
'compact',
|
||||
'limitRows',
|
||||
'descriptions',
|
||||
'limit',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
v-for="(attachment, attachmentIndex) in row.items"
|
||||
:key="attachment.id"
|
||||
class="gallery-item"
|
||||
:compact="compact"
|
||||
:nsfw="nsfw"
|
||||
:attachment="attachment"
|
||||
:size="size"
|
||||
|
|
@ -86,7 +87,7 @@
|
|||
<script src='./gallery.js'></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.Gallery {
|
||||
.gallery-rows {
|
||||
|
|
@ -100,6 +101,53 @@
|
|||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
.gallery-row-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: stretch;
|
||||
|
||||
.gallery-item {
|
||||
margin: 0 0.5em 0 0;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
// to make failed images a bit more noticeable on chromium
|
||||
min-width: 2em;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.-grid {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
||||
|
||||
.gallery-item {
|
||||
margin: 0;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-grid,
|
||||
&.-minimal {
|
||||
height: auto;
|
||||
|
||||
.gallery-row-inner {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
|
@ -114,7 +162,7 @@
|
|||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
|
@ -138,54 +186,5 @@
|
|||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-row {
|
||||
&.-grid,
|
||||
&.-minimal {
|
||||
height: auto;
|
||||
.gallery-row-inner {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-row-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-content: stretch;
|
||||
|
||||
&.-grid {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-column-gap: 0.5em;
|
||||
grid-row-gap: 0.5em;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
||||
|
||||
.gallery-item {
|
||||
margin: 0;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
margin: 0 0.5em 0 0;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
// to make failed images a bit more noticeable on chromium
|
||||
min-width: 2em;
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@
|
|||
<script src="./global_notice_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.global-notice-list {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
top: calc(var(--navbar-height) + 0.5em);
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
z-index: var(--ZI_popovers);
|
||||
z-index: var(--ZI_navbar_popovers);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -73,6 +73,7 @@
|
|||
.global-success {
|
||||
background-color: var(--alertPopupSuccess, $fallback--cGreen);
|
||||
color: var(--alertPopupSuccessText, $fallback--text);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: var(--alertPopupSuccessText, $fallback--text);
|
||||
}
|
||||
|
|
@ -81,6 +82,7 @@
|
|||
.global-info {
|
||||
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||
color: var(--alertPopupNeutralText, $fallback--text);
|
||||
|
||||
.svg-inline--fa {
|
||||
color: var(--alertPopupNeutralText, $fallback--text);
|
||||
}
|
||||
|
|
@ -88,6 +90,7 @@
|
|||
|
||||
.close-notice {
|
||||
padding-right: 0.2em;
|
||||
|
||||
.svg-inline--fa:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ const tabModeDict = {
|
|||
mentions: ['mention'],
|
||||
'likes+repeats': ['repeat', 'like'],
|
||||
follows: ['follow'],
|
||||
reactions: ['pleroma:emoji_reaction'],
|
||||
reports: ['pleroma:report'],
|
||||
moves: ['move']
|
||||
}
|
||||
|
||||
|
|
@ -12,7 +14,8 @@ const Interactions = {
|
|||
data () {
|
||||
return {
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
filterMode: tabModeDict.mentions
|
||||
filterMode: tabModeDict.mentions,
|
||||
canSeeReports: this.$store.state.users.currentUser.privileges.includes('reports_manage_reports')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@
|
|||
key="follows"
|
||||
:label="$t('interactions.follows')"
|
||||
/>
|
||||
<span
|
||||
key="reactions"
|
||||
:label="$t('interactions.emoji_reactions')"
|
||||
/>
|
||||
<span
|
||||
v-if="canSeeReports"
|
||||
key="reports"
|
||||
:label="$t('interactions.reports')"
|
||||
/>
|
||||
<span
|
||||
v-if="!allowFollowingMove"
|
||||
key="moves"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<script src="./link-preview.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.link-preview-card {
|
||||
display: flex;
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
max-width: 25%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
}
|
||||
|
||||
.card-description {
|
||||
margin: 0.5em 0 0 0;
|
||||
margin: 0.5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.list {
|
||||
&-item:not(:last-child) {
|
||||
|
|
|
|||
27
src/components/lists/lists.js
Normal file
27
src/components/lists/lists.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import ListsCard from '../lists_card/lists_card.vue'
|
||||
|
||||
const Lists = {
|
||||
data () {
|
||||
return {
|
||||
isNew: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListsCard
|
||||
},
|
||||
computed: {
|
||||
lists () {
|
||||
return this.$store.state.lists.allLists
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelNewList () {
|
||||
this.isNew = false
|
||||
},
|
||||
newList () {
|
||||
this.isNew = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Lists
|
||||
33
src/components/lists/lists.vue
Normal file
33
src/components/lists/lists.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="Lists panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('lists.lists') }}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ name: 'lists-new' }"
|
||||
class="button-default btn new-list-button"
|
||||
>
|
||||
{{ $t("lists.new") }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ListsCard
|
||||
v-for="list in lists.slice().reverse()"
|
||||
:key="list"
|
||||
:list="list"
|
||||
class="list-item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./lists.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.Lists {
|
||||
.new-list-button {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
src/components/lists_card/lists_card.js
Normal file
16
src/components/lists_card/lists_card.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEllipsisH
|
||||
)
|
||||
|
||||
const ListsCard = {
|
||||
props: [
|
||||
'list'
|
||||
]
|
||||
}
|
||||
|
||||
export default ListsCard
|
||||
52
src/components/lists_card/lists_card.vue
Normal file
52
src/components/lists_card/lists_card.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="list-card">
|
||||
<router-link
|
||||
:to="{ name: 'lists-timeline', params: { id: list.id } }"
|
||||
class="list-name"
|
||||
>
|
||||
{{ list.title }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'lists-edit', params: { id: list.id } }"
|
||||
class="button-list-edit"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./lists_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.list-card {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list-name,
|
||||
.button-list-edit {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
color: $fallback--link;
|
||||
color: var(--link, $fallback--link);
|
||||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
src/components/lists_edit/lists_edit.js
Normal file
145
src/components/lists_edit/lists_edit.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
)
|
||||
|
||||
const ListsNew = {
|
||||
components: {
|
||||
BasicUserCard,
|
||||
UserAvatar,
|
||||
ListsUserSearch,
|
||||
TabSwitcher,
|
||||
PanelLoading
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: '',
|
||||
titleDraft: '',
|
||||
membersUserIds: [],
|
||||
removedUserIds: new Set([]), // users we added for members, to undo
|
||||
searchUserIds: [],
|
||||
addedUserIds: new Set([]), // users we added from search, to undo
|
||||
searchLoading: false,
|
||||
reallyDelete: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (!this.id) return
|
||||
this.$store.dispatch('fetchList', { listId: this.id })
|
||||
.then(() => {
|
||||
this.title = this.findListTitle(this.id)
|
||||
this.titleDraft = this.title
|
||||
})
|
||||
this.$store.dispatch('fetchListAccounts', { listId: this.id })
|
||||
.then(() => {
|
||||
this.membersUserIds = this.findListAccounts(this.id)
|
||||
this.membersUserIds.forEach(userId => {
|
||||
this.$store.dispatch('fetchUserIfMissing', userId)
|
||||
})
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
id () {
|
||||
return this.$route.params.id
|
||||
},
|
||||
membersUsers () {
|
||||
return [...this.membersUserIds, ...this.addedUserIds]
|
||||
.map(userId => this.findUser(userId)).filter(user => user)
|
||||
},
|
||||
searchUsers () {
|
||||
return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
...mapGetters(['findUser', 'findListTitle', 'findListAccounts'])
|
||||
},
|
||||
methods: {
|
||||
onInput () {
|
||||
this.search(this.query)
|
||||
},
|
||||
toggleRemoveMember (user) {
|
||||
if (this.removedUserIds.has(user.id)) {
|
||||
this.id && this.addUser(user)
|
||||
this.removedUserIds.delete(user.id)
|
||||
} else {
|
||||
this.id && this.removeUser(user.id)
|
||||
this.removedUserIds.add(user.id)
|
||||
}
|
||||
},
|
||||
toggleAddFromSearch (user) {
|
||||
if (this.addedUserIds.has(user.id)) {
|
||||
this.id && this.removeUser(user.id)
|
||||
this.addedUserIds.delete(user.id)
|
||||
} else {
|
||||
this.id && this.addUser(user)
|
||||
this.addedUserIds.add(user.id)
|
||||
}
|
||||
},
|
||||
isRemoved (user) {
|
||||
return this.removedUserIds.has(user.id)
|
||||
},
|
||||
isAdded (user) {
|
||||
return this.addedUserIds.has(user.id)
|
||||
},
|
||||
addUser (user) {
|
||||
this.$store.dispatch('addListAccount', { accountId: user.id, listId: this.id })
|
||||
},
|
||||
removeUser (userId) {
|
||||
this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id })
|
||||
},
|
||||
onSearchLoading (results) {
|
||||
this.searchLoading = true
|
||||
},
|
||||
onSearchLoadingDone (results) {
|
||||
this.searchLoading = false
|
||||
},
|
||||
onSearchResults (results) {
|
||||
this.searchLoading = false
|
||||
this.searchUserIds = results
|
||||
},
|
||||
updateListTitle () {
|
||||
this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft })
|
||||
.then(() => {
|
||||
this.title = this.findListTitle(this.id)
|
||||
})
|
||||
},
|
||||
createList () {
|
||||
this.$store.dispatch('createList', { title: this.titleDraft })
|
||||
.then((list) => {
|
||||
return this
|
||||
.$store
|
||||
.dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] })
|
||||
.then(() => list.id)
|
||||
})
|
||||
.then((listId) => {
|
||||
this.$router.push({ name: 'lists-timeline', params: { id: listId } })
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'lists.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error'
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteList () {
|
||||
this.$store.dispatch('deleteList', { listId: this.id })
|
||||
this.$router.push({ name: 'lists' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListsNew
|
||||
228
src/components/lists_edit/lists_edit.vue
Normal file
228
src/components/lists_edit/lists_edit.vue
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<template>
|
||||
<div class="panel-default panel ListEdit">
|
||||
<div
|
||||
ref="header"
|
||||
class="panel-heading list-edit-heading"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled go-back-button"
|
||||
@click="$router.back"
|
||||
>
|
||||
<FAIcon
|
||||
size="lg"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
<div class="title">
|
||||
<i18n-t
|
||||
v-if="id"
|
||||
keypath="lists.editing_list"
|
||||
>
|
||||
<template #listTitle>
|
||||
{{ title }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t
|
||||
v-else
|
||||
keypath="lists.creating_list"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="input-wrap">
|
||||
<label for="list-edit-title">{{ $t('lists.title') }}</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
id="list-edit-title"
|
||||
ref="title"
|
||||
v-model="titleDraft"
|
||||
>
|
||||
<button
|
||||
v-if="id"
|
||||
class="btn button-default follow-button"
|
||||
@click="updateListTitle"
|
||||
>
|
||||
{{ $t('lists.update_title') }}
|
||||
</button>
|
||||
</div>
|
||||
<tab-switcher
|
||||
class="list-member-management"
|
||||
:scrollable-tabs="true"
|
||||
>
|
||||
<div
|
||||
v-if="id || addedUserIds.size > 0"
|
||||
:label="$t('lists.manage_members')"
|
||||
class="members-list"
|
||||
>
|
||||
<div class="users-list">
|
||||
<div
|
||||
v-for="user in membersUsers"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
>
|
||||
<button
|
||||
class="btn button-default follow-button"
|
||||
@click="toggleRemoveMember(user)"
|
||||
>
|
||||
{{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
|
||||
</button>
|
||||
</BasicUserCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="search-list"
|
||||
:label="$t('lists.add_members')"
|
||||
>
|
||||
<ListsUserSearch
|
||||
@results="onSearchResults"
|
||||
@loading="onSearchLoading"
|
||||
@loadingDone="onSearchLoadingDone"
|
||||
/>
|
||||
<div
|
||||
v-if="searchLoading"
|
||||
class="loading"
|
||||
>
|
||||
<PanelLoading />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="users-list"
|
||||
>
|
||||
<div
|
||||
v-for="user in searchUsers"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
>
|
||||
<span
|
||||
v-if="membersUserIds.includes(user.id)"
|
||||
>
|
||||
{{ $t('lists.is_in_list') }}
|
||||
</span>
|
||||
<button
|
||||
v-if="!membersUserIds.includes(user.id)"
|
||||
class="btn button-default follow-button"
|
||||
@click="toggleAddFromSearch(user)"
|
||||
>
|
||||
{{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default follow-button"
|
||||
@click="toggleRemoveMember(user)"
|
||||
>
|
||||
{{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
|
||||
</button>
|
||||
</BasicUserCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="!id"
|
||||
class="btn button-default footer-button"
|
||||
@click="createList"
|
||||
>
|
||||
{{ $t('lists.create') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!reallyDelete"
|
||||
class="btn button-default footer-button"
|
||||
@click="reallyDelete = true"
|
||||
>
|
||||
{{ $t('lists.delete') }}
|
||||
</button>
|
||||
<template v-else>
|
||||
{{ $t('lists.really_delete') }}
|
||||
<button
|
||||
class="btn button-default footer-button"
|
||||
@click="deleteList"
|
||||
>
|
||||
{{ $t('general.yes') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default footer-button"
|
||||
@click="reallyDelete = false"
|
||||
>
|
||||
{{ $t('general.no') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./lists_edit.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.ListEdit {
|
||||
--panel-body-padding: 0.5em;
|
||||
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list-edit-heading {
|
||||
grid-template-columns: auto minmax(50%, 1fr);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-member-management {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
padding-bottom: 0.7rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
& .search-list,
|
||||
& .members-list {
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.go-back-button {
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
width: var(--__panel-heading-height-inner);
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
grid-template-columns: minmax(10%, 1fr);
|
||||
|
||||
.footer-button {
|
||||
min-width: 9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/components/lists_menu/lists_menu_content.js
Normal file
22
src/components/lists_menu/lists_menu_content.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { mapState } from 'vuex'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import { getListEntries } from 'src/components/navigation/filter.js'
|
||||
|
||||
export const ListsMenuContent = {
|
||||
props: [
|
||||
'showPin'
|
||||
],
|
||||
components: {
|
||||
NavigationEntry
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
lists: getListEntries,
|
||||
currentUser: state => state.users.currentUser,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ListsMenuContent
|
||||
12
src/components/lists_menu/lists_menu_content.vue
Normal file
12
src/components/lists_menu/lists_menu_content.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<ul>
|
||||
<NavigationEntry
|
||||
v-for="item in lists"
|
||||
:key="item.name"
|
||||
:show-pin="showPin"
|
||||
:item="item"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script src="./lists_menu_content.js"></script>
|
||||
36
src/components/lists_timeline/lists_timeline.js
Normal file
36
src/components/lists_timeline/lists_timeline.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
const ListsTimeline = {
|
||||
data () {
|
||||
return {
|
||||
listId: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.list }
|
||||
},
|
||||
watch: {
|
||||
$route: function (route) {
|
||||
if (route.name === 'lists-timeline' && route.params.id !== this.listId) {
|
||||
this.listId = route.params.id
|
||||
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||
this.$store.dispatch('fetchList', { listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.listId = this.$route.params.id
|
||||
this.$store.dispatch('fetchList', { listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||
},
|
||||
unmounted () {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||
}
|
||||
}
|
||||
|
||||
export default ListsTimeline
|
||||
10
src/components/lists_timeline/lists_timeline.vue
Normal file
10
src/components/lists_timeline/lists_timeline.vue
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<Timeline
|
||||
title="list.name"
|
||||
:timeline="timeline"
|
||||
:list-id="listId"
|
||||
timeline-name="list"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./lists_timeline.js"></script>
|
||||
51
src/components/lists_user_search/lists_user_search.js
Normal file
51
src/components/lists_user_search/lists_user_search.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { debounce } from 'lodash'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
library.add(
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
)
|
||||
|
||||
const ListsUserSearch = {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
emits: ['loading', 'loadingDone', 'results'],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
query: '',
|
||||
followingOnly: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput: debounce(function () {
|
||||
this.search(this.query)
|
||||
}, 2000),
|
||||
search (query) {
|
||||
if (!query) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.$emit('loading')
|
||||
this.userIds = []
|
||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
|
||||
.then(data => {
|
||||
this.$emit('results', data.accounts.map(a => a.id))
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
this.$emit('loadingDone')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListsUserSearch
|
||||
47
src/components/lists_user_search/lists_user_search.vue
Normal file
47
src/components/lists_user_search/lists_user_search.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="ListsUserSearch">
|
||||
<div class="input-wrap">
|
||||
<div class="input-search">
|
||||
<FAIcon
|
||||
class="search-icon fa-scale-110 fa-old-padding"
|
||||
icon="search"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
ref="search"
|
||||
v-model="query"
|
||||
:placeholder="$t('lists.search')"
|
||||
@input="onInput"
|
||||
>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<Checkbox
|
||||
v-model="followingOnly"
|
||||
@change="onInput"
|
||||
>
|
||||
{{ $t('lists.following_only') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./lists_user_search.js"></script>
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.ListsUserSearch {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
<script src="./login_form.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
}
|
||||
|
||||
.login-bottom {
|
||||
margin-top: 1.0em;
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.3em 0.5em 0.6em;
|
||||
line-height:24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.form-bottom {
|
||||
|
|
@ -142,7 +142,6 @@
|
|||
|
||||
.error {
|
||||
text-align: center;
|
||||
|
||||
animation-name: shakeError;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ const MediaModal = {
|
|||
},
|
||||
type () {
|
||||
return this.currentMedia ? this.getType(this.currentMedia) : null
|
||||
},
|
||||
swipeDisableClickThreshold () {
|
||||
// If there is only one media, allow more mouse movements to close the modal
|
||||
// because there is less chance that the user wants to switch to another image
|
||||
return () => this.canNavigate ? 1 : 30
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
class="modal-image-container"
|
||||
:direction="swipeDirection"
|
||||
:threshold="swipeThreshold"
|
||||
:disable-click-threshold="swipeDisableClickThreshold"
|
||||
@preview-requested="handleSwipePreview"
|
||||
@swipe-finished="handleSwipeEnd"
|
||||
@swipeless-clicked="hide"
|
||||
|
|
@ -120,32 +121,12 @@ $modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2)
|
|||
$modal-view-button-icon-width: 3em;
|
||||
$modal-view-button-icon-margin: 0.5em;
|
||||
|
||||
.modal-view.media-modal-view {
|
||||
z-index: var(--ZI_media_modal);
|
||||
flex-direction: column;
|
||||
|
||||
.modal-view-button-arrow,
|
||||
.modal-view-button-hide {
|
||||
opacity: 0.75;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.media-modal-view {
|
||||
@keyframes media-fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
@ -226,7 +207,7 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
appearance: none;
|
||||
overflow: visible;
|
||||
cursor: pointer;
|
||||
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
|
||||
transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
|
||||
height: $modal-view-button-icon-height;
|
||||
width: $modal-view-button-icon-width;
|
||||
|
||||
|
|
@ -236,9 +217,9 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
width: $modal-view-button-icon-width;
|
||||
font-size: 1rem;
|
||||
line-height: $modal-view-button-icon-height;
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: rgba(0,0,0,.3);
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,13 +235,14 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
position: absolute;
|
||||
top: 0;
|
||||
line-height: $modal-view-button-icon-height;
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: rgba(0,0,0,.3);
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
&--prev {
|
||||
left: 0;
|
||||
|
||||
.arrow-icon {
|
||||
left: $modal-view-button-icon-margin;
|
||||
}
|
||||
|
|
@ -268,6 +250,7 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
|
||||
&--next {
|
||||
right: 0;
|
||||
|
||||
.arrow-icon {
|
||||
right: $modal-view-button-icon-margin;
|
||||
}
|
||||
|
|
@ -278,10 +261,33 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
.button-icon {
|
||||
top: $modal-view-button-icon-margin;
|
||||
right: $modal-view-button-icon-margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-view.media-modal-view {
|
||||
z-index: var(--ZI_media_modal);
|
||||
flex-direction: column;
|
||||
|
||||
.modal-view-button-arrow,
|
||||
.modal-view-button-hide {
|
||||
opacity: 0.75;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
<script src="./media_upload.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.media-upload {
|
||||
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -16,6 +17,7 @@ const MentionLink = {
|
|||
name: 'MentionLink',
|
||||
components: {
|
||||
UserAvatar,
|
||||
UnicodeDomainIndicator,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.MentionLink {
|
||||
position: relative;
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.-has-selection {
|
||||
color: var(--alertNeutralText, $fallback--text);
|
||||
background-color: var(--alertNeutral, $fallback--fg);
|
||||
|
|
@ -100,10 +101,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.full {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.serverName.-faded {
|
||||
color: var(--faintLink, $fallback--link);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@
|
|||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/><UnicodeDomainIndicator
|
||||
v-if="shouldShowFullUserName"
|
||||
:user="user"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
word-break: break-all;
|
||||
|
||||
.mention-link:not(:first-child)::before {
|
||||
content: ' ';
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.showMoreLess {
|
||||
|
|
|
|||
|
|
@ -2,33 +2,40 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
|||
import Notifications from '../notifications/notifications.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faBell,
|
||||
faBars
|
||||
faBars,
|
||||
faArrowUp,
|
||||
faMinus
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faBell,
|
||||
faBars
|
||||
faBars,
|
||||
faArrowUp,
|
||||
faMinus
|
||||
)
|
||||
|
||||
const MobileNav = {
|
||||
components: {
|
||||
SideDrawer,
|
||||
Notifications
|
||||
Notifications,
|
||||
NavigationPins
|
||||
},
|
||||
data: () => ({
|
||||
notificationsCloseGesture: undefined,
|
||||
notificationsOpen: false
|
||||
notificationsOpen: false,
|
||||
notificationsAtTop: true
|
||||
}),
|
||||
created () {
|
||||
this.notificationsCloseGesture = GestureService.swipeGesture(
|
||||
GestureService.DIRECTION_RIGHT,
|
||||
this.closeMobileNotifications,
|
||||
() => this.closeMobileNotifications(true),
|
||||
50
|
||||
)
|
||||
},
|
||||
|
|
@ -47,7 +54,10 @@ const MobileNav = {
|
|||
isChat () {
|
||||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapGetters(['unreadChatCount'])
|
||||
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']),
|
||||
chatsPinned () {
|
||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMobileSidebar () {
|
||||
|
|
@ -56,12 +66,14 @@ const MobileNav = {
|
|||
openMobileNotifications () {
|
||||
this.notificationsOpen = true
|
||||
},
|
||||
closeMobileNotifications () {
|
||||
closeMobileNotifications (markRead) {
|
||||
if (this.notificationsOpen) {
|
||||
// make sure to mark notifs seen only when the notifs were open and not
|
||||
// from close-calls.
|
||||
this.notificationsOpen = false
|
||||
this.markNotificationsAsSeen()
|
||||
if (markRead) {
|
||||
this.markNotificationsAsSeen()
|
||||
}
|
||||
}
|
||||
},
|
||||
notificationsTouchStart (e) {
|
||||
|
|
@ -73,6 +85,9 @@ const MobileNav = {
|
|||
scrollToTop () {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
scrollMobileNotificationsToTop () {
|
||||
this.$refs.mobileNotifications.scrollTo(0, 0)
|
||||
},
|
||||
logout () {
|
||||
this.$router.replace('/main/public')
|
||||
this.$store.dispatch('logout')
|
||||
|
|
@ -82,6 +97,7 @@ const MobileNav = {
|
|||
this.$store.dispatch('markNotificationsAsSeen')
|
||||
},
|
||||
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||
this.notificationsAtTop = scrollTop > 0
|
||||
if (scrollTop + clientHeight >= scrollHeight) {
|
||||
this.$refs.notifications.fetchOlderNotifications()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
<div class="item">
|
||||
<button
|
||||
class="button-unstyled mobile-nav-button"
|
||||
:title="$t('nav.mobile_sidebar')"
|
||||
:aria-expanaded="$refs.sideDrawer && !$refs.sideDrawer.closed"
|
||||
@click.stop.prevent="toggleMobileSidebar()"
|
||||
>
|
||||
<FAIcon
|
||||
|
|
@ -17,23 +19,16 @@
|
|||
icon="bars"
|
||||
/>
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
v-if="(unreadChatCount && !chatsPinned) || unreadAnnouncementCount"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</button>
|
||||
<router-link
|
||||
v-if="!hideSitename"
|
||||
class="site-name"
|
||||
:to="{ name: 'root' }"
|
||||
active-class="home"
|
||||
>
|
||||
{{ sitename }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<NavigationPins class="pins" />
|
||||
</div> <div class="item right">
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled mobile-nav-button"
|
||||
:title="unseenNotificationsCount ? $t('nav.mobile_notifications_unread_active') : $t('nav.mobile_notifications')"
|
||||
@click.stop.prevent="openMobileNotifications()"
|
||||
>
|
||||
<FAIcon
|
||||
|
|
@ -47,7 +42,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
<aside
|
||||
v-if="currentUser"
|
||||
class="mobile-notifications-drawer"
|
||||
:class="{ '-closed': !notificationsOpen }"
|
||||
|
|
@ -56,22 +51,39 @@
|
|||
>
|
||||
<div class="mobile-notifications-header">
|
||||
<span class="title">{{ $t('notifications.notifications') }}</span>
|
||||
<a
|
||||
class="mobile-nav-button"
|
||||
@click.stop.prevent="closeMobileNotifications()"
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="notificationsAtTop"
|
||||
class="button-unstyled mobile-nav-button"
|
||||
:title="$t('general.scroll_to_top')"
|
||||
@click.stop.prevent="scrollMobileNotificationsToTop"
|
||||
>
|
||||
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||
<FAIcon icon="arrow-up" />
|
||||
<FAIcon
|
||||
icon="minus"
|
||||
transform="up-7"
|
||||
/>
|
||||
</FALayers>
|
||||
</button>
|
||||
<button
|
||||
class="button-unstyled mobile-nav-button"
|
||||
:title="$t('nav.mobile_notifications_close')"
|
||||
@click.stop.prevent="closeMobileNotifications(true)"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
/>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="mobile-notifications"
|
||||
ref="mobileNotifications"
|
||||
class="mobile-notifications"
|
||||
@scroll="onScroll"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
<SideDrawer
|
||||
ref="sideDrawer"
|
||||
:logout="logout"
|
||||
|
|
@ -82,7 +94,7 @@
|
|||
<script src="./mobile_nav.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.MobileNav {
|
||||
z-index: var(--ZI_navbar);
|
||||
|
|
@ -94,6 +106,7 @@
|
|||
grid-template-columns: 2fr auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
a {
|
||||
color: var(--topBarLink, $fallback--link);
|
||||
}
|
||||
|
|
@ -114,7 +127,7 @@
|
|||
}
|
||||
|
||||
.site-name {
|
||||
padding: 0 .3em;
|
||||
padding: 0 0.3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +156,7 @@
|
|||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||
box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);
|
||||
box-shadow: var(--panelShadow);
|
||||
transition-property: transform;
|
||||
transition-duration: 0.25s;
|
||||
|
|
@ -169,22 +182,33 @@
|
|||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
||||
box-shadow: 0 0 4px rgb(0 0 0 / 60%);
|
||||
box-shadow: var(--topBarShadow);
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.3em;
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.pins {
|
||||
flex: 1;
|
||||
|
||||
.pinned-item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-notifications {
|
||||
margin-top: 50px;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
|
|
@ -194,14 +218,17 @@
|
|||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.panel:after {
|
||||
|
||||
.panel::after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.panel .panel-heading {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ library.add(
|
|||
|
||||
const HIDDEN_FOR_PAGES = new Set([
|
||||
'chats',
|
||||
'chat'
|
||||
'chat',
|
||||
'lists-edit'
|
||||
])
|
||||
|
||||
const MobilePostStatusButton = {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
v-if="isLoggedIn"
|
||||
class="MobilePostButton button-default new-status-button"
|
||||
:class="{ 'hidden': isHidden, 'always-show': isPersistent }"
|
||||
:title="$t('post_status.new_status')"
|
||||
@click="openPostForm"
|
||||
>
|
||||
<FAIcon icon="pen" />
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
<script src="./mobile_post_status_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.MobilePostButton {
|
||||
&.button-default {
|
||||
|
|
@ -29,9 +30,8 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 2px 2px rgb(0 0 0 / 30%), 0 4px 6px rgb(0 0 0 / 30%);
|
||||
z-index: 10;
|
||||
|
||||
transition: 0.35s transform;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default {
|
|||
|
||||
&.modal-background {
|
||||
pointer-events: initial;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
&.open {
|
||||
|
|
@ -69,10 +69,11 @@ export default {
|
|||
|
||||
@keyframes modal-background-fadein {
|
||||
from {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color: rgb(0 0 0 / 0%);
|
||||
}
|
||||
|
||||
to {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -41,14 +41,26 @@ const ModerationTools = {
|
|||
tagsSet () {
|
||||
return new Set(this.user.tags)
|
||||
},
|
||||
hasTagPolicy () {
|
||||
return this.$store.state.instance.tagPolicyAvailable
|
||||
canGrantRole () {
|
||||
return this.user.is_local && !this.user.deactivated && this.$store.state.users.currentUser.role === 'admin'
|
||||
},
|
||||
canChangeActivationState () {
|
||||
return this.privileged('users_manage_activation_state')
|
||||
},
|
||||
canDeleteAccount () {
|
||||
return this.privileged('users_delete')
|
||||
},
|
||||
canUseTagPolicy () {
|
||||
return this.$store.state.instance.tagPolicyAvailable && this.privileged('users_manage_tags')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasTag (tagName) {
|
||||
return this.tagsSet.has(tagName)
|
||||
},
|
||||
privileged (privilege) {
|
||||
return this.$store.state.users.currentUser.privileges.includes(privilege)
|
||||
},
|
||||
toggleTag (tag) {
|
||||
const store = this.$store
|
||||
if (this.tagsSet.has(tag)) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<span v-if="canGrantRole">
|
||||
<button
|
||||
class="button-default dropdown-item"
|
||||
@click="toggleRight("admin")"
|
||||
|
|
@ -24,28 +24,31 @@
|
|||
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="canChangeActivationState || canDeleteAccount"
|
||||
role="separator"
|
||||
class="dropdown-divider"
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
v-if="canChangeActivationState"
|
||||
class="button-default dropdown-item"
|
||||
@click="toggleActivationStatus()"
|
||||
>
|
||||
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteAccount"
|
||||
class="button-default dropdown-item"
|
||||
@click="deleteUserDialog(true)"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.delete_account') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="hasTagPolicy"
|
||||
v-if="canUseTagPolicy"
|
||||
role="separator"
|
||||
class="dropdown-divider"
|
||||
/>
|
||||
<span v-if="hasTagPolicy">
|
||||
<span v-if="canUseTagPolicy">
|
||||
<button
|
||||
class="button-default dropdown-item"
|
||||
@click="toggleTag(tags.FORCE_NSFW)"
|
||||
|
|
@ -163,18 +166,21 @@
|
|||
<script src="./moderation_tools.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import "../../variables";
|
||||
|
||||
.moderation-tools-popover {
|
||||
height: 100%;
|
||||
|
||||
.trigger {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
display: flex !important;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.moderation-tools-button {
|
||||
svg,i {
|
||||
svg,
|
||||
i {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue