Merge branch 'develop' into 'tusooa/1222-in-reply-to'

# Conflicts:
#   src/components/status/status.vue
This commit is contained in:
HJ 2024-12-26 23:40:16 +00:00
commit 6335a937c9
464 changed files with 29571 additions and 7423 deletions

View file

@ -9,6 +9,3 @@
</template>
<script src="./about.js"></script>
<style lang="scss">
</style>

View file

@ -2,6 +2,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 ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisV
@ -16,14 +17,30 @@ const AccountActions = {
'user', 'relationship'
],
data () {
return { }
return {
showingConfirmBlock: false,
showingConfirmRemoveFollower: false
}
},
components: {
ProgressButton,
Popover,
UserListMenu
UserListMenu,
ConfirmModal
},
methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
showRepeats () {
this.$store.dispatch('showReblogs', this.user.id)
},
@ -31,13 +48,29 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id)
},
blockUser () {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
removeUserFromFollowers () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
this.hideConfirmRemoveUserFromFollowers()
},
reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
@ -50,6 +83,12 @@ const AccountActions = {
}
},
computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
})

View file

@ -11,14 +11,14 @@
<template v-if="relationship.following">
<button
v-if="relationship.showing_reblogs"
class="btn button-default dropdown-item"
class="dropdown-item menu-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!relationship.showing_reblogs"
class="btn button-default dropdown-item"
class="dropdown-item menu-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
@ -31,34 +31,34 @@
<UserListMenu :user="user" />
<button
v-if="relationship.followed_by"
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="removeUserFromFollowers"
>
{{ $t('user_card.remove_follower') }}
</button>
<button
v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
<button
v-if="pleromaChatMessagesAvailable"
class="btn button-default btn-block dropdown-item"
class="dropdown-item menu-item"
@click="openChat"
>
{{ $t('user_card.message') }}
@ -74,24 +74,62 @@
</button>
</template>
</Popover>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmBlock"
:title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')"
@accepted="doBlockUser"
@cancelled="hideConfirmBlock"
>
<i18n-t
keypath="user_card.block_confirm"
tag="span"
scope="global"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
scope="global"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div>
</template>
<script src="./account_actions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.AccountActions {
.ellipsis-button {
width: 2.5em;
margin: -0.5em 0;
padding: 0.5em 0;
text-align: center;
&:not(:hover) .icon {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
}
}
</style>

View file

@ -0,0 +1,57 @@
export default {
name: 'Alert',
selector: '.alert',
validInnerComponents: [
'Text',
'Icon',
'Link',
'Border',
'ButtonUnstyled'
],
variants: {
normal: '.neutral',
error: '.error',
warning: '.warning',
success: '.success'
},
editor: {
border: 1,
aspect: '3 / 1'
},
defaultRules: [
{
directives: {
background: '--text',
opacity: 0.5,
blur: '9px'
}
},
{
parent: {
component: 'Alert'
},
component: 'Border',
directives: {
textColor: '--parent'
}
},
{
variant: 'error',
directives: {
background: '--cRed'
}
},
{
variant: 'warning',
directives: {
background: '--cOrange'
}
},
{
variant: 'success',
directives: {
background: '--cGreen'
}
}
]
}

View file

@ -99,22 +99,20 @@
<script src="./announcement.js"></script>
<style lang="scss">
@import "../../variables";
.announcement {
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-bottom: 1px solid var(--border);
border-radius: 0;
padding: var(--status-margin, $status-margin);
padding: var(--status-margin);
.heading, .body {
margin-bottom: var(--status-margin, $status-margin);
.heading,
.body {
margin-bottom: var(--status-margin);
}
.footer {
display: flex;
flex-direction: column;
.times {
display: flex;
flex-direction: column;

View file

@ -3,7 +3,7 @@
<textarea
ref="textarea"
v-model="announcement.content"
class="post-textarea"
class="input post-textarea"
rows="1"
cols="1"
:placeholder="$t('announcements.post_placeholder')"
@ -14,6 +14,7 @@
<input
id="announcement-start-time"
v-model="announcement.startsAt"
class="input"
:type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled"
>
@ -23,6 +24,7 @@
<input
id="announcement-end-time"
v-model="announcement.endsAt"
class="input"
:type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled"
>
@ -32,8 +34,9 @@
id="announcement-all-day"
v-model="announcement.allDay"
:disabled="disabled"
/>
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
>
{{ $t('announcements.all_day_prompt') }}
</Checkbox>
</span>
</div>
</template>

View file

@ -1,9 +1,9 @@
<template>
<div class="panel panel-default announcements-page">
<div class="panel-heading">
<span>
<h1 class="title">
{{ $t('announcements.page_header') }}
</span>
</h1>
</div>
<div class="panel-body">
<section
@ -61,14 +61,13 @@
<script src="./announcements_page.js"></script>
<style lang="scss">
@import "../../variables";
.announcements-page {
.post-form {
padding: var(--status-margin, $status-margin);
padding: var(--status-margin);
.heading, .body {
margin-bottom: var(--status-margin, $status-margin);
.heading,
.body {
margin-bottom: var(--status-margin);
}
.post-button {

View file

@ -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>

View file

@ -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,

View file

@ -1,5 +1,3 @@
@import '../../_variables.scss';
.Attachment {
display: inline-flex;
flex-direction: column;
@ -9,10 +7,8 @@
height: 100%;
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-radius: var(--roundness);
border-color: var(--border);
.attachment-wrapper {
flex: 1 1 auto;
@ -84,6 +80,13 @@
}
}
.video-container {
border: none;
outline: none;
color: inherit;
background: transparent;
}
.audio-container {
display: flex;
align-items: flex-end;
@ -102,14 +105,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;
@ -127,23 +129,26 @@
.attachment-button {
padding: 0;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
border-radius: var(--roundness);
text-align: center;
width: 2em;
height: 2em;
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);
}
}
.svg-inline--fa {
color: rgba(0, 0, 0, 0.6);
}
&.-contain-fit {
img,
canvas {
object-fit: contain;
}
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
&.-cover-fit {
img,
canvas {
object-fit: cover;
}
}
@ -160,8 +165,9 @@
.image {
flex: 1;
img {
border: 0px;
border: 0;
border-radius: 5px;
height: 100%;
object-fit: cover;
@ -172,9 +178,10 @@
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 1rem;
margin: 0px;
margin: 0;
}
}
}
@ -202,8 +209,7 @@
&.-placeholder {
display: inline-block;
color: $fallback--link;
color: var(--postLink, $fallback--link);
color: var(--link);
overflow: hidden;
white-space: nowrap;
height: auto;
@ -252,17 +258,9 @@
cursor: progress;
}
&.-contain-fit {
img,
canvas {
object-fit: contain;
}
}
&.-cover-fit {
img,
canvas {
object-fit: cover;
&.-compact {
.placeholder-container {
padding-bottom: 0.5em;
}
}
}

View file

@ -0,0 +1,25 @@
export default {
name: 'Attachment',
selector: '.Attachment',
notEditable: true,
validInnerComponents: [
'Border',
'ButtonUnstyled',
'Input'
],
defaultRules: [
{
directives: {
roundness: 3
}
},
{
component: 'ButtonUnstyled',
parent: { component: 'Attachment' },
directives: {
background: '#FFFFFF',
opacity: 0.5
}
}
]
}

View file

@ -38,7 +38,7 @@
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
class="input description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
@ -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>
@ -174,7 +175,6 @@
:is="videoTag"
v-if="type === 'video' && !hidden"
class="video-container"
:class="{ 'button-unstyled': 'isModal' }"
:href="attachment.url"
@click.stop.prevent="openModal"
>
@ -252,7 +252,7 @@
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
class="input description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>

View file

@ -1,3 +1,4 @@
<!-- FIXME THIS NEEDS TO BE REFACTORED TO USE POPOVER -->
<template>
<div
v-click-outside="onClickOutside"
@ -6,12 +7,12 @@
<input
v-model="term"
:placeholder="placeholder"
class="autosuggest-input"
class="input autosuggest-input"
@click="onInputClick"
>
<div
v-if="resultsVisible && filtered.length > 0"
class="autosuggest-results"
class="panel autosuggest-results"
>
<slot
v-for="item in filtered"
@ -24,8 +25,6 @@
<script src="./autosuggest.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.autosuggest {
position: relative;
@ -40,18 +39,15 @@
top: 100%;
right: 0;
max-height: 400px;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
background-color: var(--bg);
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
border-color: var(--border);
border-radius: var(--roundness);
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);
box-shadow: var(--shadow);
overflow-y: auto;
z-index: 1;
}

View file

@ -17,8 +17,6 @@
<script src="./avatar_list.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.avatars {
display: flex;
margin: 0;
@ -36,8 +34,7 @@
}
.avatar-small {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
border-radius: var(--roundness);
height: 24px;
width: 24px;
}

View file

@ -0,0 +1,30 @@
export default {
name: 'Badge',
selector: '.badge',
validInnerComponents: [
'Text',
'Icon'
],
variants: {
notification: '.-notification'
},
defaultRules: [
{
component: 'Root',
directives: {
'--badgeNotification': 'color | --cRed'
}
},
{
directives: {
background: '--cGreen'
}
},
{
variant: 'notification',
directives: {
background: '--cRed'
}
}
]
}

View file

@ -47,9 +47,8 @@
display: flex;
flex: 1 0;
margin: 0;
padding: 0.6em 1em;
--emoji-size: 14px;
--emoji-size: 1em;
&-collapsed-content {
margin-left: 0.7em;

View file

@ -37,6 +37,7 @@
.block-card-content-container {
margin-top: 0.5em;
text-align: right;
button {
width: 10em;
}

View file

@ -0,0 +1,22 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faEllipsisH
)
const BookmarkFolderCard = {
props: [
'folder',
'allBookmarks'
],
computed: {
firstLetter () {
return this.folder ? this.folder.name[0] : null
}
}
}
export default BookmarkFolderCard

View file

@ -0,0 +1,111 @@
<template>
<div
v-if="allBookmarks"
class="bookmark-folder-card"
>
<router-link
:to="{ name: 'bookmarks' }"
class="bookmark-folder-name"
>
<span class="icon">
<FAIcon
fixed-width
class="fa-scale-110 menu-icon"
icon="bookmark"
/>
</span>{{ $t('nav.all_bookmarks') }}
</router-link>
</div>
<div
v-else
class="bookmark-folder-card"
>
<router-link
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
class="bookmark-folder-name"
>
<img
v-if="folder.emoji_url"
class="iconEmoji iconEmoji-image"
:src="folder.emoji_url"
:alt="folder.emoji"
:title="folder.emoji"
>
<span
v-else-if="folder.emoji"
class="iconEmoji"
>
<span>
{{ folder.emoji }}
</span>
</span>
<span
v-else-if="firstLetter"
class="icon iconLetter fa-scale-110"
>{{ firstLetter }}</span>{{ folder.name }}
</router-link>
<router-link
:to="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
class="button-folder-edit"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="ellipsis-h"
/>
</router-link>
</div>
</template>
<script src="./bookmark_folder_card.js"></script>
<style lang="scss">
.bookmark-folder-card {
display: flex;
align-items: center;
}
a.bookmark-folder-name {
display: flex;
align-items: center;
flex-grow: 1;
.icon,
.iconLetter,
.iconEmoji {
display: inline-block;
height: 2.5rem;
width: 2.5rem;
margin-right: 0.5rem;
}
.icon,
.iconLetter {
font-size: 1.5rem;
line-height: 2.5rem;
text-align: center;
}
.iconEmoji {
text-align: center;
object-fit: contain;
vertical-align: middle;
> span {
font-size: 1.5rem;
line-height: 2.5rem;
}
}
img.iconEmoji {
padding: 0.25em;
box-sizing: border-box;
}
}
.bookmark-folder-name,
.button-folder-edit {
margin: 0;
padding: 1em;
color: var(--link);
}
</style>

View file

@ -0,0 +1,80 @@
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import apiService from '../../services/api/api.service'
const BookmarkFolderEdit = {
data () {
return {
name: '',
nameDraft: '',
emoji: '',
emojiUrl: null,
emojiDraft: '',
emojiUrlDraft: null,
emojiPickerExpanded: false,
reallyDelete: false
}
},
components: {
EmojiPicker
},
created () {
if (!this.id) return
const credentials = this.$store.state.users.currentUser.credentials
apiService.fetchBookmarkFolders({ credentials })
.then((folders) => {
const folder = folders.find(folder => folder.id === this.id)
if (!folder) return
this.nameDraft = this.name = folder.name
this.emojiDraft = this.emoji = folder.emoji
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
})
},
computed: {
id () {
return this.$route.params.id
}
},
methods: {
selectEmoji (event) {
this.emojiDraft = event.insertion
this.emojiUrlDraft = event.insertionUrl
},
showEmojiPicker () {
if (!this.emojiPickerExpanded) {
this.$refs.picker.showPicker()
}
},
onShowPicker () {
this.emojiPickerExpanded = true
},
onClosePicker () {
this.emojiPickerExpanded = false
},
updateFolder () {
this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
},
createFolder () {
this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
.catch((e) => {
this.$store.dispatch('pushGlobalNotice', {
messageKey: 'bookmark_folders.error',
messageArgs: [e.message],
level: 'error'
})
})
},
deleteFolder () {
this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id })
this.$router.push({ name: 'bookmark-folders' })
}
}
}
export default BookmarkFolderEdit

View file

@ -0,0 +1,200 @@
<template>
<div class="panel-default panel BookmarkFolderEdit">
<div
ref="header"
class="panel-heading folder-edit-heading"
>
<button
class="button-unstyled go-back-button"
@click="$router.back"
>
<FAIcon
size="lg"
icon="chevron-left"
/>
</button>
<h1 class="title">
<i18n-t
v-if="id"
keypath="bookmark_folders.editing_folder"
scope="global"
>
<template #folderName>
{{ name }}
</template>
</i18n-t>
<i18n-t
v-else
keypath="bookmark_folders.creating_folder"
scope="global"
/>
</h1>
</div>
<div class="panel-body">
<div class="input-wrap">
<label for="folder-edit-title">{{ $t('bookmark_folders.emoji') }}</label>
<button
class="input input-emoji"
:title="$t('bookmark_folder.emoji_pick')"
@click="showEmojiPicker"
>
<img
v-if="emojiUrlDraft"
class="iconEmoji iconEmoji-image"
:src="emojiUrlDraft"
:alt="emojiDraft"
:title="emojiDraft"
>
<span
v-else-if="emojiDraft"
class="iconEmoji"
>
<span>
{{ emojiDraft }}
</span>
</span>
</button>
<EmojiPicker
ref="picker"
class="emoji-picker-panel"
@emoji="selectEmoji"
@show="onShowPicker"
@close="onClosePicker"
/>
</div>
<div class="input-wrap">
<label for="folder-edit-title">{{ $t('bookmark_folders.name') }}</label>
<input
id="folder-edit-title"
ref="name"
v-model="nameDraft"
class="input"
>
</div>
</div>
<div class="panel-footer">
<span class="spacer" />
<button
v-if="!id"
class="btn button-default footer-button"
@click="createFolder"
>
{{ $t('bookmark_folders.create') }}
</button>
<button
v-else-if="!reallyDelete"
class="btn button-default footer-button"
@click="reallyDelete = true"
>
{{ $t('bookmark_folders.delete') }}
</button>
<template v-else>
{{ $t('bookmark_folders.really_delete') }}
<button
class="btn button-default footer-button"
@click="deleteFolder"
>
{{ $t('general.yes') }}
</button>
<button
class="btn button-default footer-button"
@click="reallyDelete = false"
>
{{ $t('general.no') }}
</button>
</template>
<div
v-if="id && !reallyDelete"
>
<button
class="btn button-default follow-button"
@click="updateFolder"
>
{{ $t('bookmark_folders.update_folder') }}
</button>
</div>
</div>
</div>
</template>
<script src="./bookmark_folder_edit.js"></script>
<style lang="scss">
.BookmarkFolderEdit {
--panel-body-padding: 0.5em;
overflow: hidden;
display: flex;
flex-direction: column;
.folder-edit-heading {
grid-template-columns: auto minmax(50%, 1fr);
}
.panel-body {
display: flex;
gap: 0.5em;
}
.emoji-picker-panel {
position: absolute;
z-index: 20;
margin-top: 2px;
&.hide {
display: none;
}
}
.input-emoji {
height: 2.5em;
width: 2.5em;
padding: 0;
.iconEmoji {
display: inline-block;
text-align: center;
object-fit: contain;
vertical-align: middle;
height: 2.5em;
width: 2.5em;
> span {
font-size: 1.5rem;
line-height: 2.5rem;
}
}
img.iconEmoji {
padding: 0.25em;
box-sizing: border-box;
}
}
.input-wrap {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.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>

View file

@ -0,0 +1,27 @@
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
const BookmarkFolders = {
data () {
return {
isNew: false
}
},
components: {
BookmarkFolderCard
},
computed: {
bookmarkFolders () {
return this.$store.state.bookmarkFolders.allFolders
}
},
methods: {
cancelNewFolder () {
this.isNew = false
},
newFolder () {
this.isNew = true
}
}
}
export default BookmarkFolders

View file

@ -0,0 +1,37 @@
<template>
<div class="Bookmark-folders panel panel-default">
<div class="panel-heading">
<h1 class="title">
{{ $t('nav.bookmark_folders') }}
</h1>
<router-link
:to="{ name: 'bookmark-folder-new' }"
class="button-default btn new-folder-button"
>
{{ $t("bookmark_folders.new") }}
</router-link>
</div>
<div class="panel-body">
<BookmarkFolderCard
:all-bookmarks="true"
class="list-item"
/>
<BookmarkFolderCard
v-for="folder in bookmarkFolders.slice().reverse()"
:key="folder"
:folder="folder"
class="list-item"
/>
</div>
</div>
</template>
<script src="./bookmark_folders.js"></script>
<style lang="scss">
.Bookmark-folders {
.new-folder-button {
padding: 0 0.5em;
}
}
</style>

View file

@ -0,0 +1,16 @@
import { mapState } from 'vuex'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
export const BookmarkFoldersMenuContent = {
components: {
NavigationEntry
},
computed: {
...mapState({
folders: getBookmarkFolderEntries
})
}
}
export default BookmarkFoldersMenuContent

View file

@ -0,0 +1,19 @@
<template>
<ul>
<NavigationEntry
:item="{
name: 'bookmarks',
routeObject: { name: 'bookmarks' },
label: 'nav.all_bookmarks',
icon: 'bookmark'
}"
/>
<NavigationEntry
v-for="item in folders"
:key="item.id"
:item="item"
/>
</ul>
</template>
<script src="./bookmark_folders_menu_content.js"></script>

View file

@ -1,16 +1,31 @@
import Timeline from '../timeline/timeline.vue'
const Bookmarks = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
created () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
},
components: {
Timeline
},
computed: {
folderId () {
return this.$route.params.id
},
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
},
watch: {
folderId () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
}
},
unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
}
}

View file

@ -3,6 +3,7 @@
:title="$t('nav.bookmarks')"
:timeline="timeline"
:timeline-name="'bookmarks'"
:bookmark-folder-id="folderId"
/>
</template>

View file

@ -0,0 +1,13 @@
export default {
name: 'Border',
selector: '/*border*/',
virtual: true,
defaultRules: [
{
directives: {
textColor: '$mod(--parent 10)',
textAuto: 'no-auto'
}
}
]
}

View file

@ -0,0 +1,129 @@
export default {
name: 'Button', // Name of the component
selector: '.button-default', // CSS selector/prefix
// outOfTreeSelector: '' // out-of-tree selector is used when other components are laid over it but it's not part of the tree, see Underlay component
// States, system witll calculate ALL possible combinations of those and prepend "normal" to them + standalone "normal" state
states: {
// States are a bit expensive - the amount of combinations generated is about (1/6)n^3+n, so adding more state increased number of combination by an order of magnitude!
// All states inherit from "normal" state, there is no other inheirtance, i.e. hover+disabled only inherits from "normal", not from hover nor disabled.
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
// normal: '' // normal state is implicitly added, it is always included
toggled: '.toggled',
focused: ':focus-visible',
pressed: ':focus:active',
hover: ':hover:not(:disabled)',
disabled: ':disabled'
},
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
variants: {
// Variants save on computation time since adding new variant just adds one more "set".
// normal: '', // you can override normal variant, it will be appenended to the main class
danger: '.danger'
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
// This (currently) is further multipled by number of places where component can exist.
},
editor: {
aspect: '2 / 1'
},
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
validInnerComponents: [
'Text',
'Icon'
],
// Default rules, used as "default theme", essentially.
defaultRules: [
{
component: 'Root',
directives: {
'--buttonDefaultHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--buttonDefaultFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
'--buttonPressedBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
}
},
{
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look
// like within it
directives: {
background: '--fg',
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
roundness: 3
}
},
{
state: ['hover'],
directives: {
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
}
},
{
state: ['focused'],
directives: {
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
}
},
{
state: ['pressed'],
directives: {
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
}
},
{
state: ['pressed', 'hover'],
directives: {
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
}
},
{
state: ['toggled'],
directives: {
background: '--inheritedBackground,-14.2',
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
}
},
{
state: ['toggled', 'hover'],
directives: {
background: '--inheritedBackground,-14.2',
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
}
},
{
state: ['toggled', 'disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonPressedBevel']
}
},
{
state: ['disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonDefaultBevel']
}
},
{
component: 'Text',
parent: {
component: 'Button',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'Button',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]
}

View file

@ -0,0 +1,97 @@
export default {
name: 'ButtonUnstyled',
selector: '.button-unstyled',
notEditable: true,
states: {
toggled: '.toggled',
disabled: ':disabled',
hover: ':hover:not(:disabled)',
focused: ':focus-within'
},
validInnerComponents: [
'Text',
'Icon',
'Badge'
],
defaultRules: [
{
directives: {
background: '#ffffff',
opacity: 0,
shadow: []
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['hover']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'hover']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'focused']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['toggled', 'focused', 'hover']
},
directives: {
textColor: '--parent--text'
}
},
{
component: 'Text',
parent: {
component: 'ButtonUnstyled',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]
}

View file

@ -11,15 +11,15 @@
.chat-view-body {
box-sizing: border-box;
background-color: var(--chatBg, $fallback--bg);
display: flex;
flex-direction: column;
width: 100%;
overflow: visible;
min-height: calc(100vh - var(--navbar-height));
margin: 0 0 0 0;
border-radius: 10px 10px 0 0;
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
margin: 0;
border-radius: var(--roundness);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
&::after {
border-radius: 0;
@ -37,8 +37,6 @@
.footer {
position: sticky;
bottom: 0;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
z-index: 1;
}
@ -61,12 +59,10 @@
position: absolute;
right: 1.3em;
top: -3.2em;
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
display: flex;
justify-content: center;
align-items: center;
box-shadow: 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);
@ -79,12 +75,6 @@
visibility: visible;
}
i {
font-size: 1em;
color: $fallback--text;
color: var(--text, $fallback--text);
}
.unread-message-count {
font-size: 0.8em;
left: 50%;

View file

@ -0,0 +1,19 @@
export default {
name: 'Chat',
selector: '.chat-message-list',
validInnerComponents: [
'Text',
'Link',
'Icon',
'Avatar',
'ChatMessage'
],
defaultRules: [
{
directives: {
background: '--bg',
blur: '5px'
}
}
]
}

View file

@ -26,7 +26,7 @@
</div>
</div>
<div
class="message-list"
class="chat-message-list message-list"
:style="{ height: scrollableContainerHeight }"
>
<template v-if="!errorLoadingChat">
@ -61,7 +61,7 @@
<FAIcon icon="chevron-down" />
<div
v-if="newMessageCount"
class="badge badge-notification unread-chat-count unread-message-count"
class="badge -notification unread-chat-count unread-message-count"
>
{{ newMessageCount }}
</div>
@ -95,6 +95,5 @@
<script src="./chat.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import './chat.scss';
@import "./chat";
</style>

View file

@ -7,9 +7,9 @@
class="chat-list panel panel-default"
>
<div class="panel-heading -sticky">
<span class="title">
<h1 class="title">
{{ $t("chats.chats") }}
</span>
</h1>
<button
class="button-default"
@click="newChat"
@ -45,8 +45,6 @@
<script src="./chat_list.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.chat-list {
min-height: 25em;
margin-bottom: 0;
@ -57,8 +55,7 @@
font-size: 1.2em;
display: flex;
justify-content: center;
color: $fallback--text;
color: var(--faint, $fallback--text);
color: var(--textFaint);
}
</style>

View file

@ -1,8 +1,6 @@
.chat-list-item {
display: flex;
flex-direction: row;
padding: 0.75em;
height: 5em;
overflow: hidden;
box-sizing: border-box;
cursor: pointer;
@ -11,11 +9,6 @@
outline: none;
}
&:hover {
background-color: var(--selectedPost, $fallback--lightBg);
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1);
}
.chat-list-item-left {
margin-right: 1em;
}
@ -29,7 +22,7 @@
.heading {
width: 100%;
display: inline-flex;
display: flex;
justify-content: space-between;
line-height: 1em;
}
@ -47,18 +40,17 @@
}
.chat-preview {
display: inline-flex;
display: flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0.35em 0;
color: $fallback--text;
color: var(--faint, $fallback--text);
color: var(--textFaint);
width: 100%;
}
a {
color: var(--faintLink, $fallback--link);
color: var(--linkFaint);
text-decoration: none;
pointer-events: none;
}
@ -67,25 +59,19 @@
canvas {
display: none;
}
img {
visibility: visible;
}
}
.Avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
.chat-preview-body {
--emoji-size: 1.4em;
padding-right: 1em;
}
.time-wrapper {
line-height: var(--post-line-height);
}
.chat-preview-body {
padding-right: 1em;
}
}

View file

@ -36,7 +36,7 @@
/>
<div
v-if="chat.unread > 0"
class="badge badge-notification unread-chat-count"
class="badge -notification unread-chat-count"
>
{{ chat.unread }}
</div>
@ -48,6 +48,5 @@
<script src="./chat_list_item.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import './chat_list_item.scss';
@import "./chat_list_item";
</style>

View file

@ -1,12 +1,10 @@
@import '../../_variables.scss';
.chat-message-wrapper {
&.hovered-message-chain {
.animated.Avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
@ -27,11 +25,6 @@
.menu-icon {
cursor: pointer;
&:hover, .extra-button-popover.open & {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
.popover {
@ -54,39 +47,25 @@
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);
background-color: var(--background);
color: var(--text);
border-radius: var(--roundness);
display: flex;
padding: 0.75em;
border: 1px solid var(--border);
}
.created-at {
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,57 +82,34 @@
}
.pending {
.status-content.media-body, .created-at {
.status-content.media-body,
.created-at {
color: var(--faint);
}
}
.error {
.status-content.media-body, .created-at {
color: $fallback--cRed;
color: var(--badgeNotification, $fallback--cRed);
.status-content.media-body,
.created-at {
color: var(--badgeNotification);
}
}
.incoming {
a {
color: var(--chatMessageIncomingLink, $fallback--link);
}
.status {
color: var(--chatMessageIncomingText, $fallback--text);
background-color: var(--chatMessageIncomingBg, $fallback--bg);
border: 1px solid var(--chatMessageIncomingBorder, --border);
}
.created-at {
a {
color: var(--chatMessageIncomingText, $fallback--text);
}
}
.chat-message-menu {
left: 0.4rem;
}
.chat-message-inner {
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 80%;
min-width: 10em;
width: 100%;
}
.outgoing {
display: flex;
flex-direction: row;
flex-wrap: wrap;
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;
}
@ -163,10 +119,23 @@
}
}
.incoming {
.chat-message-menu {
left: 0.4rem;
}
}
.chat-message-inner.with-media {
width: 100%;
.status {
width: 100%;
}
}
.visible {
opacity: 1;
}
}
.chat-message-date-separator {
@ -174,6 +143,5 @@
margin: 1.4em 0;
font-size: 0.9em;
user-select: none;
color: $fallback--text;
color: var(--faintedText, $fallback--text);
color: var(--textFaint);
}

View file

@ -0,0 +1,30 @@
export default {
name: 'ChatMessage',
selector: '.chat-message',
variants: {
outgoing: '.outgoing'
},
validInnerComponents: [
'Text',
'Icon',
'Border',
'Button',
'RichContent',
'Attachment',
'PollGraph'
],
defaultRules: [
{
directives: {
background: '--bg, 2',
backgroundNoCssColor: 'yes'
}
},
{
variant: 'outgoing',
directives: {
background: '--bg, 5'
}
}
]
}

View file

@ -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"
>
@ -53,7 +53,7 @@
<template #content>
<div class="dropdown-menu">
<button
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
@click="deleteMessage"
>
<FAIcon icon="times" /> {{ $t("chats.delete") }}
@ -98,6 +98,6 @@
<script src="./chat_message.js"></script>
<style lang="scss">
@import './chat_message.scss';
@import "./chat_message";
</style>

View file

@ -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%;
@ -16,11 +16,6 @@
padding-bottom: 0.7rem;
}
.basic-user-card:hover {
cursor: pointer;
background-color: var(--selectedPost, $fallback--lightBg);
}
.go-back-button {
text-align: center;
line-height: 1;

View file

@ -16,27 +16,29 @@
/>
</button>
</div>
<div class="input-wrap">
<div class="input-search">
<FAIcon
class="search-icon fa-scale-110 fa-old-padding"
icon="search"
/>
<div class="panel-body">
<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"
class="input"
placeholder="Search people"
@input="onInput"
>
</div>
<input
ref="search"
v-model="query"
placeholder="Search people"
@input="onInput"
>
</div>
<div class="member-list">
<div
v-for="user in availableUsers"
:key="user.id"
class="member"
>
<div @click.capture.prevent="goToChat(user)">
<div class="member-list">
<div
v-for="user in availableUsers"
:key="user.id"
class="list-item"
@click.capture.prevent="goToChat(user)"
>
<BasicUserCard :user="user" />
</div>
</div>
@ -46,6 +48,5 @@
<script src="./chat_new.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import './chat_new.scss';
@import "./chat_new";
</style>

View file

@ -26,15 +26,13 @@
<script src="./chat_title.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.chat-title {
display: flex;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
--emoji-size: 14px;
--emoji-size: 1em;
.username {
max-width: 100%;
@ -54,8 +52,7 @@
margin-right: 0.5em;
height: 1.5em;
width: 1.5em;
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
border-radius: var(--roundness);
&.animated::before {
display: none;

View file

@ -1,19 +1,33 @@
<template>
<label
class="checkbox"
:class="{ disabled, indeterminate }"
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
>
<span
v-if="!!$slots.before"
class="label -before"
:class="{ faint: disabled }"
>
<slot name="before" />
</span>
<input
type="checkbox"
class="visible-for-screenreader-only"
:disabled="disabled"
:checked="modelValue"
:indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)"
>
<i class="checkbox-indicator" />
<i
class="input -checkbox checkbox-indicator"
:aria-hidden="true"
:class="{ disabled }"
@transitionend.capture="onTransitionEnd"
/>
<span
v-if="!!$slots.default"
class="label"
class="label -after"
:class="{ faint: disabled }"
>
<slot />
</span>
@ -27,38 +41,63 @@ export default {
'indeterminate',
'disabled'
],
emits: ['update:modelValue']
emits: ['update:modelValue'],
data: (vm) => ({
indeterminateTransitionFix: vm.indeterminate
}),
watch: {
indeterminate (e) {
if (e) {
this.indeterminateTransitionFix = true
}
}
},
methods: {
onTransitionEnd (e) {
if (!this.indeterminate) {
this.indeterminateTransitionFix = false
}
}
}
}
</script>
<style lang="scss">
@import '../../_variables.scss';
@import "../../mixins";
.checkbox {
position: relative;
display: inline-block;
min-height: 1.2em;
&-indicator {
&-indicator,
& .label {
vertical-align: middle;
}
& > &-indicator {
/* Reset .input stuff */
padding: 0;
margin: 0;
position: relative;
padding-left: 1.2em;
line-height: inherit;
display: inline-block;
width: 1.2em;
height: 1.2em;
box-shadow: none;
}
&-indicator::before {
position: absolute;
right: 0;
top: 0;
inset: 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: var(--inputShadow);
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
border-radius: var(--roundness);
box-shadow: var(--shadow);
background-color: var(--background);
vertical-align: top;
text-align: center;
line-height: 1.1em;
@ -68,35 +107,37 @@ export default {
box-sizing: border-box;
}
&.disabled {
.checkbox-indicator::before,
.label {
opacity: .5;
}
.label {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
.disabled {
.checkbox-indicator::before {
background-color: var(--background);
}
}
input[type=checkbox] {
display: none;
input[type="checkbox"] {
&:checked + .checkbox-indicator::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
color: var(--text);
}
&:indeterminate + .checkbox-indicator::before {
content: '';
color: $fallback--text;
color: var(--inputText, $fallback--text);
content: "";
color: var(--text);
}
}
& > span {
margin-left: .5em;
&.indeterminate-fix {
input[type="checkbox"] + .checkbox-indicator::before {
content: "";
}
}
& > .label {
&.-after {
margin-left: 0.5em;
}
&.-before {
margin-right: 0.5em;
}
}
}
</style>

View file

@ -1,19 +1,23 @@
@import '../../_variables.scss';
.color-input {
display: inline-flex;
.label {
flex: 1 1 auto;
}
.opt {
margin-right: 0.5em;
}
&-field.input {
display: inline-flex;
flex: 0 0 0;
max-width: 9em;
align-items: stretch;
padding: .2em 8px;
input {
color: var(--text);
background: none;
color: $fallback--lightText;
color: var(--inputText, $fallback--lightText);
border: none;
padding: 0;
margin: 0;
@ -23,46 +27,79 @@
min-width: 3em;
padding: 0;
}
}
&.nativeColor {
flex: 0 0 2em;
min-width: 2em;
align-self: stretch;
min-height: 100%;
.nativeColor {
cursor: pointer;
flex: 0 0 auto;
padding: 0;
input {
appearance: none;
max-width: 0;
min-width: 0;
max-height: 0;
/* stylelint-disable-next-line declaration-no-important */
opacity: 0 !important;
}
}
.computedIndicator,
.validIndicator,
.invalidIndicator,
.transparentIndicator {
flex: 0 0 2em;
margin: 0.2em 0.5em;
min-width: 2em;
align-self: stretch;
min-height: 100%;
min-height: 1.1em;
border-radius: var(--roundness);
}
.invalidIndicator {
background: transparent;
box-sizing: border-box;
border: 2px solid var(--cRed);
}
.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;
border-top-left-radius: var(--roundness);
}
&::before {
bottom: 0;
right: 0;
border-bottom-right-radius: var(--roundness);
}
}
&.disabled,
&:disabled {
.nativeColor input,
.computedIndicator,
.validIndicator,
.invalidIndicator,
.transparentIndicator {
/* stylelint-disable-next-line declaration-no-important */
opacity: 0.25 !important;
}
}
}
.label {
flex: 1 1 auto;
}
}

View file

@ -6,49 +6,77 @@
<label
:for="name"
class="label"
:class="{ faint: !present || disabled }"
>
{{ label }}
</label>
<Checkbox
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
v-if="typeof fallback !== 'undefined' && showOptionalCheckbox && !hideOptionalCheckbox"
:model-value="present"
:disabled="disabled"
class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
@update:modelValue="updateValue(typeof modelValue === 'undefined' ? fallback : undefined)"
/>
<div class="input color-input-field">
<div
class="input color-input-field"
:class="{ disabled: !present || disabled }"
>
<input
:id="name + '-t'"
class="textColor unstyled"
:class="{ disabled: !present || disabled }"
type="text"
:value="modelValue || fallback"
:disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)"
>
<input
v-if="validColor"
:id="name"
class="nativeColor unstyled"
type="color"
:value="modelValue || fallback"
:disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)"
@input="updateValue($event.target.value)"
>
<div
v-if="transparentColor"
v-if="validColor"
class="validIndicator"
:style="{backgroundColor: modelValue || fallback}"
/>
<div
v-else-if="transparentColor"
class="transparentIndicator"
/>
<div
v-if="computedColor"
v-else-if="computedColor"
class="computedIndicator"
:style="{backgroundColor: fallback}"
/>
<div
v-else
class="invalidIndicator"
/>
<label class="nativeColor">
<FAIcon icon="eye-dropper" />
<input
:id="name"
class="unstyled"
type="color"
:value="modelValue || fallback"
:disabled="!present || disabled"
:class="{ disabled: !present || disabled }"
@input="updateValue($event.target.value)"
>
</label>
</div>
</div>
</template>
<script>
import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { throttle } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEyeDropper
} from '@fortawesome/free-solid-svg-icons'
library.add(
faEyeDropper
)
export default {
components: {
Checkbox
@ -84,10 +112,16 @@ export default {
default: false
},
// Show "optional" tickbox, for when value might become mandatory
showOptionalTickbox: {
showOptionalCheckbox: {
required: false,
type: Boolean,
default: true
},
// Force "optional" tickbox to hide
hideOptionalCheckbox: {
required: false,
type: Boolean,
default: false
}
},
emits: ['update:modelValue'],
@ -102,18 +136,14 @@ export default {
return this.modelValue === 'transparent'
},
computedColor () {
return this.modelValue && this.modelValue.startsWith('--')
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
}
},
methods: {
updateValue: throttle(function (value) {
this.$emit('update:modelValue', value)
}, 100)
}
}
</script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss">
.color-control {
input.text-input {
max-width: 7em;
flex: 1;
}
}
</style>

View file

@ -0,0 +1,323 @@
<template>
<div
class="ComponentPreview"
:class="{ '-shadow-controls': shadowControl }"
>
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
<component
:is="'style'"
v-html="previewCss"
/>
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
<label
v-show="shadowControl"
role="heading"
class="header"
:class="{ faint: disabled }"
>
{{ $t('settings.style.shadows.offset') }}
</label>
<label
v-show="shadowControl && !hideControls"
class="x-shift-number"
>
{{ $t('settings.style.shadows.offset-x') }}
<input
:value="shadow?.x"
:disabled="disabled"
:class="{ disabled }"
class="input input-number"
type="number"
@input="e => updateProperty('x', e.target.value)"
>
</label>
<label
v-show="shadowControl && !hideControls"
class="y-shift-number"
>
{{ $t('settings.style.shadows.offset-y') }}
<input
:value="shadow?.y"
:disabled="disabled"
:class="{ disabled }"
class="input input-number"
type="number"
@input="e => updateProperty('y', e.target.value)"
>
</label>
<input
v-show="shadowControl && !hideControls"
:value="shadow?.x"
:disabled="disabled"
:class="{ disabled }"
class="input input-range x-shift-slider"
type="range"
max="20"
min="-20"
@input="e => updateProperty('x', e.target.value)"
>
<input
v-show="shadowControl && !hideControls"
:value="shadow?.y"
:disabled="disabled"
:class="{ disabled }"
class="input input-range y-shift-slider"
type="range"
max="20"
min="-20"
@input="e => updateProperty('y', e.target.value)"
>
<div
class="preview-window"
:class="{ '-light-grid': lightGrid }"
>
<div
class="preview-block"
:class="previewClass"
:style="style"
>
{{ $t('settings.style.themes3.editor.test_string') }}
</div>
<div
v-if="invalid"
class="invalid-container"
>
<div class="alert error invalid-label">
{{ $t('settings.style.themes3.editor.invalid') }}
</div>
</div>
</div>
<div class="assists">
<Checkbox
v-model="lightGrid"
name="lightGrid"
class="input-light-grid"
>
{{ $t('settings.style.shadows.light_grid') }}
</Checkbox>
<div class="style-control">
<label class="label">
{{ $t('settings.style.shadows.zoom') }}
</label>
<input
v-model="zoom"
class="input input-number y-shift-number"
type="number"
>
</div>
<ColorInput
v-if="!noColorControl"
v-model="colorOverride"
class="input-color-input"
fallback="#606060"
:label="$t('settings.style.shadows.color_override')"
/>
</div>
</div>
</template>
<script>
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue'
export default {
components: {
Checkbox,
ColorInput
},
props: [
'shadow',
'shadowControl',
'previewClass',
'previewStyle',
'previewCss',
'disabled',
'invalid',
'noColorControl'
],
emits: ['update:shadow'],
data () {
return {
colorOverride: undefined,
lightGrid: false,
zoom: 100
}
},
computed: {
style () {
const result = [
this.previewStyle,
`zoom: ${this.zoom / 100}`
]
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
return result
},
hideControls () {
return typeof this.shadow === 'string'
}
},
methods: {
updateProperty (axis, value) {
this.$emit('update:shadow', { axis, value: Number(value) })
}
}
}
</script>
<style lang="scss">
.ComponentPreview {
display: grid;
grid-template-columns: 1em 1fr 1fr 1em;
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
grid-template-areas:
"header header header header "
"preview preview preview y-slide"
"preview preview preview y-slide"
"preview preview preview y-slide"
"x-slide x-slide x-slide . "
"x-num x-num y-num y-num "
"assists assists assists assists";
grid-gap: 0.5em;
&:not(.-shadow-controls) {
grid-template-areas:
"header header header header "
"preview preview preview y-slide"
"preview preview preview y-slide"
"preview preview preview y-slide"
"assists assists assists assists";
grid-template-rows: 2em 1fr 1fr 1fr max-content;
}
.header {
grid-area: header;
justify-self: center;
align-self: baseline;
line-height: 2;
}
.invalid-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: grid;
align-items: center;
justify-items: center;
background-color: rgba(100 0 0 / 50%);
.alert {
padding: 0.5em 1em;
}
}
.assists {
grid-area: assists;
display: grid;
grid-auto-flow: rows;
grid-auto-rows: 2em;
grid-gap: 0.5em;
}
.input-light-grid {
justify-self: center;
}
.input-number {
min-width: 2em;
}
.x-shift-number {
grid-area: x-num;
justify-self: right;
}
.y-shift-number {
grid-area: y-num;
justify-self: left;
}
.x-shift-number,
.y-shift-number {
input {
max-width: 4em;
}
}
.x-shift-slider {
grid-area: x-slide;
height: auto;
align-self: start;
min-width: 10em;
}
.y-shift-slider {
grid-area: y-slide;
writing-mode: vertical-lr;
justify-self: left;
min-height: 10em;
}
.x-shift-slider,
.y-shift-slider {
padding: 0;
}
.preview-window {
--__grid-color1: rgb(102 102 102);
--__grid-color2: rgb(153 153 153);
--__grid-color1-disabled: rgba(102 102 102 / 20%);
--__grid-color2-disabled: rgba(153 153 153 / 20%);
&.-light-grid {
--__grid-color1: rgb(205 205 205);
--__grid-color2: rgb(255 255 255);
--__grid-color1-disabled: rgba(205 205 205 / 20%);
--__grid-color2-disabled: rgba(255 255 255 / 20%);
}
position: relative;
grid-area: preview;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 10em;
min-height: 10em;
background-color: var(--__grid-color2);
background-image:
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
border-radius: var(--roundness);
&.disabled {
background-color: var(--__grid-color2-disabled);
background-image:
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
}
.preview-block {
background: var(--background, var(--bg));
display: flex;
justify-content: center;
align-items: center;
min-width: 33%;
min-height: 33%;
max-width: 80%;
max-height: 80%;
border-width: 0;
border-style: solid;
border-color: var(--border);
border-radius: var(--roundness);
box-shadow: var(--shadow);
}
}
}
</style>

View file

@ -0,0 +1,37 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
/**
* This component emits the following events:
* cancelled, emitted when the action should not be performed;
* accepted, emitted when the action should be performed;
*
* The caller should close this dialog after receiving any of the two events.
*/
const ConfirmModal = {
components: {
DialogModal
},
props: {
title: {
type: String
},
cancelText: {
type: String
},
confirmText: {
type: String
}
},
computed: {
},
methods: {
onCancel () {
this.$emit('cancelled')
},
onAccept () {
this.$emit('accepted')
}
}
}
export default ConfirmModal

View file

@ -0,0 +1,29 @@
<template>
<dialog-modal
v-body-scroll-lock="true"
class="confirm-modal"
:on-cancel="onCancel"
>
<template #header>
<span v-text="title" />
</template>
<slot />
<template #footer>
<button
class="btn button-default"
@click.prevent="onAccept"
v-text="confirmText"
/>
<button
class="btn button-default"
@click.prevent="onCancel"
v-text="cancelText"
/>
</template>
</dialog-modal>
</template>
<script src="./confirm_modal.js"></script>

View file

@ -3,39 +3,62 @@
v-if="contrast"
class="contrast-ratio"
>
<span
:title="hint"
<span v-if="showRatio">
{{ contrast.text }}
</span>
<Tooltip
:text="hint"
class="rating"
>
<span v-if="contrast.aaa">
<FAIcon icon="thumbs-up" />
<FAIcon
icon="thumbs-up"
:size="showRatio ? 'lg' : ''"
/>
</span>
<span v-if="!contrast.aaa && contrast.aa">
<FAIcon icon="adjust" />
<FAIcon
icon="adjust"
:size="showRatio ? 'lg' : ''"
/>
</span>
<span v-if="!contrast.aaa && !contrast.aa">
<FAIcon icon="exclamation-triangle" />
<FAIcon
icon="exclamation-triangle"
:size="showRatio ? 'lg' : ''"
/>
</span>
</span>
<span
</Tooltip>
<Tooltip
v-if="contrast && large"
:text="hint_18pt"
class="rating"
:title="hint_18pt"
>
<span v-if="contrast.laaa">
<FAIcon icon="thumbs-up" />
<FAIcon
icon="thumbs-up"
:size="showRatio ? 'large' : ''"
/>
</span>
<span v-if="!contrast.laaa && contrast.laa">
<FAIcon icon="adjust" />
<FAIcon
icon="adjust"
:size="showRatio ? 'lg' : ''"
/>
</span>
<span v-if="!contrast.laaa && !contrast.laa">
<FAIcon icon="exclamation-triangle" />
<FAIcon
icon="exclamation-triangle"
:size="showRatio ? 'lg' : ''"
/>
</span>
</span>
</Tooltip>
</span>
</template>
<script>
import Tooltip from 'src/components/tooltip/tooltip.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAdjust,
@ -50,6 +73,9 @@ library.add(
)
export default {
components: {
Tooltip
},
props: {
large: {
required: false,
@ -62,6 +88,11 @@ export default {
required: false,
type: Object,
default: () => ({})
},
showRatio: {
required: false,
type: Boolean,
default: false
}
},
computed: {
@ -87,9 +118,7 @@ export default {
.contrast-ratio {
display: flex;
justify-content: flex-end;
margin-top: -4px;
margin-bottom: 5px;
align-items: baseline;
.label {
margin-right: 1em;
@ -97,7 +126,6 @@ export default {
.rating {
display: inline-block;
text-align: center;
margin-left: 0.5em;
}
}

View file

@ -56,7 +56,8 @@ const conversation = {
expanded: false,
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {},
inlineDivePosition: null
inlineDivePosition: null,
loadStatusError: null
}
},
props: [
@ -392,11 +393,15 @@ const conversation = {
this.setHighlight(this.originalStatusId)
})
} else {
this.loadStatusError = null
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
.then((status) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation()
})
.catch((error) => {
this.loadStatusError = error
})
}
},
getReplies (id) {

View file

@ -9,7 +9,9 @@
v-if="isExpanded"
class="panel-heading conversation-heading -sticky"
>
<span class="title"> {{ $t('timeline.conversation') }} </span>
<h1 class="title">
{{ $t('timeline.conversation') }}
</h1>
<button
v-if="collapsable"
class="button-unstyled -link"
@ -28,7 +30,27 @@
class="rightside-button"
/>
</div>
<div class="conversation-body panel-body">
<div
v-if="isPage && !status"
class="conversation-body"
:class="{ 'panel-body': isExpanded }"
>
<p v-if="!loadStatusError">
<FAIcon
spin
icon="circle-notch"
/>
{{ $t('status.loading') }}
</p>
<p v-else>
{{ $t('status.load_error', { error: loadStatusError }) }}
</p>
</div>
<div
v-else
class="conversation-body"
:class="{ 'panel-body': isExpanded }"
>
<div
v-if="isTreeView"
class="thread-body"
@ -203,6 +225,7 @@
</div>
<div
v-else
class="Conversation -hidden"
:style="hiddenStyle"
/>
</template>
@ -210,17 +233,19 @@
<script src="./conversation.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.Conversation {
z-index: 1;
&.-hidden {
background: var(--__panel-background);
backdrop-filter: var(--__panel-backdrop-filter);
}
.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);
padding: var(--status-margin);
border-bottom: 1px solid var(--border);
border-radius: 0;
/* Make the button stretch along the whole row */
display: flex;
align-items: stretch;
@ -228,67 +253,82 @@
}
.thread-ancestors {
margin-left: var(--status-margin, $status-margin);
border-left: 2px solid var(--border, $fallback--border);
margin-left: var(--status-margin);
border-left: 2px solid var(--border);
}
.thread-ancestor.-faded .StatusContent {
--link: var(--faintLink);
--text: var(--faint);
color: var(--text);
.thread-ancestor.-faded .RichContent {
/* stylelint-disable declaration-no-important */
--text: var(--textFaint) !important;
--link: var(--linkFaint) !important;
--funtextGreentext: var(--funtextGreentextFaint) !important;
--funtextCyantext: var(--funtextCyantextFaint) !important;
/* stylelint-enable declaration-no-important */
}
.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);
padding-left: var(--status-margin);
border-bottom: 1px solid var(--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);
padding: var(--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);
border-radius: 0;
}
.thread-ancestor-has-other-replies .conversation-status,
&:last-child:not(.-expanded) .conversation-status,
&.-expanded .conversation-status:last-child,
.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);
}
/* expanded conversation in timeline */
&.status-fadein.-expanded .thread-body {
border-left-width: 4px;
border-left-style: solid;
border-left-color: $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);
border-bottom: 1px solid var(--border, $fallback--border);
border-left: 4px solid var(--cRed);
border-radius: var(--roundness);
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom: 1px solid var(--border);
}
&.-expanded.status-fadein {
margin: calc(var(--status-margin, $status-margin) / 2);
--___margin: calc(var(--status-margin) / 2);
background: var(--background);
margin: var(--___margin);
&::before {
z-index: -1;
content: "";
display: block;
position: absolute;
top: calc(var(--___margin) * -1);
bottom: calc(var(--___margin) * -1);
left: calc(var(--___margin) * -1);
right: calc(var(--___margin) * -1);
background: var(--background);
backdrop-filter: var(--__panel-backdrop-filter);
}
}
}
</style>

View file

@ -1,4 +1,5 @@
import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSignInAlt,
@ -30,7 +31,8 @@ library.add(
export default {
components: {
SearchBar
SearchBar,
ConfirmModal
},
data: () => ({
searchBarHidden: true,
@ -40,7 +42,8 @@ export default {
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
)
),
showingConfirmLogout: false
}),
computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
@ -73,21 +76,41 @@ export default {
hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private }
privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
}
},
methods: {
scrollToTop () {
window.scrollTo(0, 0)
},
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
this.hideConfirmLogout()
},
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
this.$store.dispatch('openSettingsModal', 'user')
},
openAdminModal () {
this.$store.dispatch('openSettingsModal', 'admin')
}
}
}

View file

@ -1,5 +1,3 @@
@import '../../_variables.scss';
.DesktopNav {
width: 100%;
z-index: var(--ZI_navbar);
@ -9,7 +7,7 @@
}
a {
color: var(--topBarLink, $fallback--link);
color: var(--link);
}
.inner-nav {
@ -27,20 +25,13 @@
--miniColumn: 25rem;
--maxiColumn: 45rem;
--columnGap: 1em;
max-width: calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--columnGap)
);
}
&.-column-stretch.-wide .inner-nav {
max-width: calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--notifsColumnWidth, var(--miniColumn)) +
var(--columnGap)
);
max-width:
calc(
var(--sidebarColumnWidth, var(--miniColumn)) +
var(--contentColumnWidth, var(--maxiColumn)) +
var(--columnGap)
);
}
&.-logoLeft .inner-nav {
@ -48,29 +39,20 @@
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 {
color: $fallback--text;
color: var(--btnTopBarText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedTopBar, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedTopBarText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledTopBarText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg)
&,
svg {
color: var(--text);
}
}
@ -82,6 +64,7 @@
transition-duration: 100ms;
@media all and (min-width: 800px) {
/* stylelint-disable-next-line declaration-no-important */
opacity: 1 !important;
}
@ -89,8 +72,7 @@
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $fallback--fg;
background-color: var(--topBarText, $fallback--fg);
background-color: var(--text);
position: absolute;
top: 0;
bottom: 0;
@ -111,8 +93,7 @@
text-align: center;
.svg-inline--fa {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
color: var(--link);
}
}

View file

@ -20,6 +20,7 @@
class="logo"
:to="{ name: 'root' }"
:style="logoBgStyle"
:title="sitename"
>
<div
class="mask"
@ -38,44 +39,55 @@
/>
<button
class="button-unstyled nav-icon"
@click="openSettingsModal"
:title="$t('nav.preferences')"
@click.stop="openSettingsModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
:title="$t('nav.preferences')"
/>
</button>
<a
<button
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
class="nav-icon"
class="button-unstyled nav-icon"
target="_blank"
@click.stop
:title="$t('nav.administration')"
@click.stop="openAdminModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
:title="$t('nav.administration')"
/>
</a>
</button>
<span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
@click.prevent="logout"
:title="$t('login.logout')"
@click.stop.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
:title="$t('login.logout')"
/>
</button>
</div>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</nav>
</template>
<script src="./desktop_nav.js"></script>

View file

@ -8,11 +8,11 @@
@click.stop=""
>
<div class="panel-heading dialog-modal-heading">
<div class="title">
<h1 class="title">
<slot name="header" />
</div>
</h1>
</div>
<div class="dialog-modal-content">
<div class="panel-body dialog-modal-content">
<slot name="default" />
</div>
<div class="dialog-modal-footer user-interactions panel-footer">
@ -25,8 +25,6 @@
<script src="./dialog_modal.js"></script>
<style lang="scss">
@import '../../_variables.scss';
// TODO: unify with other modals.
.dark-overlay {
&::before {
@ -38,8 +36,8 @@
position: fixed;
right: 0;
top: 0;
background: rgba(27,31,35,.5);
z-index: 99;
background: rgb(27 31 35 / 50%);
z-index: 2000;
}
}
@ -51,11 +49,9 @@
margin: 15vh auto;
position: fixed;
transform: translateX(-50%);
z-index: 999;
z-index: 2001;
cursor: default;
display: block;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
.dialog-modal-heading {
.title {
@ -65,25 +61,20 @@
.dialog-modal-content {
margin: 0;
padding: 1rem 1rem;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
padding: 1rem;
white-space: normal;
}
.dialog-modal-footer {
margin: 0;
padding: .5em .5em;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
border-top: 1px solid $fallback--border;
border-top: 1px solid var(--border, $fallback--border);
padding: 0.5em;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
button {
width: auto;
margin-left: .5rem;
margin-left: 0.5rem;
}
}
}

View file

@ -6,7 +6,9 @@
>
<div class="edit-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.edit_status') }}
<h1 class="title">
{{ $t('post_status.edit_status') }}
</h1>
</div>
<PostStatusForm
class="panel-body"
@ -26,6 +28,7 @@
.modal-view.edit-form-modal-view {
align-items: flex-start;
}
.edit-form-modal-panel {
flex-shrink: 0;
margin-top: 25%;

View file

@ -1,6 +1,8 @@
import Completion from '../../services/completion/completion.js'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.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'
@ -109,9 +111,10 @@ const EmojiInput = {
},
data () {
return {
randomSeed: genRandomSeed(),
input: undefined,
caretEl: undefined,
highlighted: 0,
highlighted: -1,
caret: 0,
focused: false,
blurTimeout: null,
@ -125,12 +128,16 @@ const EmojiInput = {
components: {
Popover,
EmojiPicker,
UnicodeDomainIndicator
UnicodeDomainIndicator,
ScreenReaderNotice
},
computed: {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
defaultCandidateIndex () {
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
},
preText () {
return this.modelValue.slice(0, this.caret)
},
@ -203,6 +210,12 @@ const EmojiInput = {
top: this.input.scrollTop,
left: this.input.scrollLeft
})
},
suggestionListId () {
return `suggestions-${this.randomSeed}`
},
suggestionItemId () {
return (index) => `suggestion-item-${index}-${this.randomSeed}`
}
},
mounted () {
@ -278,6 +291,11 @@ const EmojiInput = {
...rest,
img: imageUrl || ''
}))
this.highlighted = this.defaultCandidateIndex
this.$refs.screenReaderNotice.announce(
this.$tc('tool_tip.autocomplete_available',
this.suggestions.length,
{ number: this.suggestions.length }))
}
},
methods: {
@ -374,26 +392,27 @@ const EmojiInput = {
},
cycleBackward (e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted -= 1
if (this.highlighted < 0) {
this.highlighted = this.suggestions.length - 1
}
this.highlighted -= 1
if (this.highlighted === -1) {
this.input.focus()
} else if (this.highlighted < -1) {
this.highlighted = len - 1
}
if (len > 0) {
e.preventDefault()
} else {
this.highlighted = 0
}
},
cycleForward (e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = 0
}
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = -1
this.input.focus()
}
if (len > 0) {
e.preventDefault()
} else {
this.highlighted = 0
}
},
scrollIntoView () {
@ -540,6 +559,13 @@ const EmojiInput = {
})
},
resize () {
},
autoCompleteItemLabel (suggestion) {
if (suggestion.user) {
return suggestion.displayText + ' ' + suggestion.detailText
} else {
return this.maybeLocalizedEmojiName(suggestion)
}
}
}
}

View file

@ -1,15 +1,22 @@
<template>
<div
ref="root"
class="emoji-input"
class="input emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
>
<slot />
<slot
:id="'textbox-' + randomSeed"
:aria-owns="suggestionListId"
aria-autocomplete="both"
:aria-expanded="showSuggestions"
:aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
/>
<!-- TODO: make the 'x' disappear if at the end maybe? -->
<div
ref="hiddenOverlay"
class="hidden-overlay"
:style="overlayStyle"
:aria-hidden="true"
>
<span>{{ preText }}</span>
<span
@ -18,11 +25,16 @@
>x</span>
<span>{{ postText }}</span>
</div>
<screen-reader-notice
ref="screenReaderNotice"
aria-live="assertive"
/>
<template v-if="enableEmojiPicker">
<button
v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon"
type="button"
:title="$t('emoji.add_emoji')"
@click.prevent="togglePicker"
>
<FAIcon :icon="['far', 'smile-beam']" />
@ -43,17 +55,24 @@
ref="suggestorPopover"
class="autocomplete-panel"
placement="bottom"
:trigger-attrs="{ 'aria-hidden': true }"
>
<template #content>
<div
:id="suggestionListId"
ref="panel-body"
class="autocomplete-panel-body"
role="listbox"
>
<div
v-for="(suggestion, index) in suggestions"
:id="suggestionItemId(index)"
:key="index"
class="autocomplete-item"
:class="{ highlighted: index === highlighted }"
class="menu-item autocomplete-item"
role="option"
:class="{ '-active': index === highlighted }"
:aria-label="autoCompleteItemLabel(suggestion)"
:aria-selected="index === highlighted"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">
@ -91,29 +110,23 @@
<script src="./emoji_input.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.emoji-input {
.input.emoji-input {
padding: 0;
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;
&:hover i {
color: $fallback--text;
color: var(--text, $fallback--text);
color: var(--text);
}
}
@ -123,12 +136,23 @@
margin-top: 2px;
&.hide {
display: none
display: none;
}
}
input, textarea {
input,
textarea {
flex: 1 0 auto;
color: inherit;
/* stylelint-disable-next-line declaration-no-important */
background: none !important;
box-shadow: none;
border: none;
outline: none;
}
&.with-picker input {
padding-right: 30px;
}
.hidden-overlay {
@ -140,8 +164,10 @@
right: 0;
left: 0;
overflow: hidden;
/* DEBUG STUFF */
color: red;
/* set opacity to non-zero to see the overlay */
.caret {
@ -151,32 +177,34 @@
}
}
}
.autocomplete {
&-panel {
position: absolute;
}
&-item {
&-item.menu-item {
display: flex;
cursor: pointer;
padding: 0.2em 0.4em;
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
height: 32px;
padding-top: 0;
padding-bottom: 0;
.image {
width: 32px;
height: 32px;
line-height: 32px;
width: calc(var(--__line-height) + var(--__vertical-gap) * 2);
height: calc(var(--__line-height) + var(--__vertical-gap) * 2);
line-height: var(--__line-height);
text-align: center;
font-size: 32px;
margin-right: 4px;
margin-right: var(--__horizontal-gap);
img {
width: 32px;
height: 32px;
width: calc(var(--__line-height) + var(--__vertical-gap) * 2);
height: calc(var(--__line-height) + var(--__vertical-gap) * 2);
object-fit: contain;
}
span {
font-size: var(--__line-height);
line-height: var(--__line-height);
}
}
.label {
@ -194,16 +222,6 @@
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>

View file

@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
const newSuggestions = state.users.users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
user.screen_name && user.name && (
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix))
).slice(0, 20).sort((a, b) => {
let aScore = 0
let bScore = 0

View file

@ -95,11 +95,21 @@ const toHeaderId = id => {
const EmojiPicker = {
props: {
enableStickerPicker: {
required: false,
type: Boolean,
default: true
},
hideCustomEmoji: {
required: false,
type: Boolean,
default: false
}
},
inject: {
popoversZLayer: {
default: ''
}
},
data () {
return {
keyword: '',
@ -108,11 +118,13 @@ const EmojiPicker = {
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiTimeout: null,
hideCustomEmojiInPicker: false,
// Lazy-load only after the first time `showing` becomes true.
contentLoaded: false,
groupRefs: {},
emojiRefs: {},
filteredEmojiGroups: [],
emojiSize: 0,
width: 0
}
},
@ -123,9 +135,28 @@ const EmojiPicker = {
Popover
},
methods: {
updateEmojiSize () {
const css = window.getComputedStyle(this.$refs.popover.$el)
const emojiSize = css.getPropertyValue('--emojiSize')
const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '')
const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, ''))
const fontSize = css.getPropertyValue('font-size').replace(/[^0-9,.]+/, '')
let emojiSizeReal
if (emojiSizeUnit.endsWith('em')) {
emojiSizeReal = emojiSizeValue * fontSize
} else {
emojiSizeReal = emojiSizeValue
}
const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSize)
this.emojiSize = fullEmojiSize
},
showPicker () {
this.$refs.popover.showPopover()
this.onShowing()
this.$nextTick(() => {
this.onShowing()
})
},
hidePicker () {
this.$refs.popover.hidePopover()
@ -153,7 +184,7 @@ const EmojiPicker = {
if (!this.keepOpen) {
this.$refs.popover.hidePopover()
}
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen })
},
onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = this.$refs['emoji-groups'].$el
@ -217,6 +248,7 @@ const EmojiPicker = {
},
onShowing () {
const oldContentLoaded = this.contentLoaded
this.updateEmojiSize()
this.recalculateItemPerRow()
this.$nextTick(() => {
this.$refs.search.focus()
@ -244,7 +276,7 @@ const EmojiPicker = {
if (!this.$refs['emoji-groups']) {
return
}
this.width = this.$refs['emoji-groups'].$el.offsetWidth
this.width = this.$refs['emoji-groups'].$el.clientWidth
})
}
},
@ -259,16 +291,20 @@ const EmojiPicker = {
},
computed: {
minItemSize () {
return this.emojiHeight
return this.emojiSize
},
// used to watch it
fontSize () {
this.$nextTick(() => {
this.updateEmojiSize()
})
return this.$store.getters.mergedConfig.fontSize
},
emojiHeight () {
return 32 + 4
},
emojiWidth () {
return 32 + 4
return this.emojiSize
},
itemPerRow () {
return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6
return this.width ? Math.floor(this.width / this.emojiSize) : 6
},
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
@ -280,6 +316,9 @@ const EmojiPicker = {
return 0
},
allCustomGroups () {
if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
return {}
}
const emojis = this.$store.getters.groupedCustomEmojis
if (emojis.unpacked) {
emojis.unpacked.text = this.$t('emoji.unpacked')
@ -342,6 +381,9 @@ const EmojiPicker = {
return emoji.displayText
}
},
isInModal () {
return this.popoversZLayer === 'modals'
}
}
}

View file

@ -1,90 +1,77 @@
@import '../../_variables.scss';
$emoji-picker-header-height: 36px;
$emoji-picker-header-picture-width: 32px;
$emoji-picker-header-picture-height: 32px;
$emoji-picker-emoji-size: 32px;
.emoji-picker {
--__emoji-picker-header: 2.2em;
width: 25em;
max-width: 100vw;
max-width: calc(100vw - 20px); // popover gives 10px margin from window edge
display: flex;
flex-direction: column;
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;
width: var(--__emoji-picker-header);
max-width: var(--__emoji-picker-header);
height: var(--__emoji-picker-header);
max-height: var(--__emoji-picker-header);
.still-image {
max-width: 100%;
max-height: 100%;
height: 100%;
width: 100%;
width: var(--__emoji-picker-header);
max-width: var(--__emoji-picker-header);
height: var(--__emoji-picker-header);
max-height: var(--__emoji-picker-header);
object-fit: contain;
--_still_image-label-scale: 0.5;
}
}
.keep-open,
.too-many-emoji {
padding: 7px;
.too-many-emoji,
.hide-custom-emoji {
padding: 0.5em;
line-height: normal;
}
.hide-custom-emoji {
padding-top: 0;
}
.too-many-emoji {
display: flex;
flex-direction: column;
}
.keep-open-label {
padding: 0 7px;
padding: 0 0.5em;
display: flex;
}
.heading {
display: flex;
padding: 10px 7px 5px;
padding: 0.7em 0.5em 0;
}
.content {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0px;
min-height: 0;
}
.emoji-tabs {
flex-grow: 1;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-flow: row nowrap;
overflow-x: auto;
}
.emoji-groups {
height: 100%;
min-height: 200px;
overflow-y: hidden;
}
.additional-tabs {
display: flex;
flex: 1;
border-left: 1px solid;
border-left-color: $fallback--icon;
border-left-color: var(--icon, $fallback--icon);
padding-left: 7px;
border-left-color: var(--border);
padding-left: 0.5em;
flex: 0 0 auto;
}
@ -93,36 +80,35 @@ $emoji-picker-emoji-size: 32px;
flex-basis: auto;
display: flex;
align-content: center;
scrollbar-width: thin;
&-item {
padding: 0 7px;
padding: 0 0.5em;
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;
width: var(--__emoji-picker-header);
max-width: var(--__emoji-picker-header);
height: var(--__emoji-picker-header);
max-height: var(--__emoji-picker-header);
display: flex;
align-items: center;
.svg-inline--fa {
font-size: 1.85em;
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
&.active {
border-bottom: 4px solid;
svg {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
&.toggled {
border-bottom: 0.2em solid;
}
}
}
.sticker-picker {
flex: 1 1 auto
flex: 1 1 auto;
}
.stickers,
@ -143,7 +129,7 @@ $emoji-picker-emoji-size: 32px;
.emoji {
&-search {
padding: 5px;
padding: 0.3em;
flex: 0 0 auto;
input {
@ -152,22 +138,28 @@ $emoji-picker-emoji-size: 32px;
}
&-groups {
height: 100%;
min-height: 200px;
flex: 1 1 1px;
position: relative;
overflow: auto;
scrollbar-gutter: stable both-edges;
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;
}
@ -178,13 +170,13 @@ $emoji-picker-emoji-size: 32px;
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 0.85em;
width: 100%;
margin: 0;
padding-left: 0.3em;
&.disabled {
display: none;
@ -193,28 +185,30 @@ $emoji-picker-emoji-size: 32px;
}
&-item {
width: $emoji-picker-emoji-size;
height: $emoji-picker-emoji-size;
width: var(--emoji-size);
height: var(--emoji-size);
box-sizing: border-box;
display: flex;
line-height: $emoji-picker-emoji-size;
line-height: var(--emoji-size);
align-items: center;
justify-content: center;
margin: 4px;
margin: 0.2em;
cursor: pointer;
.emoji-picker-emoji.-custom {
object-fit: contain;
max-width: 100%;
max-height: 100%;
width: var(--emoji-size);
max-width: var(--emoji-size);
height: var(--emoji-size);
max-height: var(--emoji-size);
--_still_image-label-scale: 0.5;
}
.emoji-picker-emoji.-unicode {
font-size: 24px;
font-size: 1.6em;
overflow: hidden;
}
}
}
}

View file

@ -3,24 +3,32 @@
ref="popover"
trigger="click"
popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true, tabindex: -1 }"
@show="onPopoverShown"
@close="onPopoverClosed"
>
<template #content>
<div class="heading">
<!--
Body scroll lock needs to be on every scrollable element on safari iOS.
Here we tell it to enable scrolling for this element.
See https://github.com/willmcpo/body-scroll-lock#vanilla-js
-->
<span
ref="header"
v-body-scroll-lock="isInModal"
class="emoji-tabs"
>
<span
v-for="group in filteredEmojiGroups"
:ref="setGroupRef('group-header-' + group.id)"
:key="group.id"
class="emoji-tabs-item"
class="button-unstyled emoji-tabs-item"
:class="{
active: activeGroupView === group.id
toggled: activeGroupView === group.id
}"
:title="group.text"
role="button"
@click.prevent="highlight(group.id)"
>
<span
@ -44,8 +52,8 @@
class="additional-tabs"
>
<span
class="stickers-tab-icon additional-tabs-item"
:class="{active: showingStickers}"
class="button-unstyled stickers-tab-icon additional-tabs-item"
:class="{toggled: showingStickers}"
:title="$t('emoji.stickers')"
@click.prevent="toggleStickers"
>
@ -69,20 +77,24 @@
ref="search"
v-model="keyword"
type="text"
class="form-control"
class="input form-control"
:placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false"
>
</div>
<!-- Enables scrolling for this element on safari iOS. See comments for header. -->
<DynamicScroller
ref="emoji-groups"
v-body-scroll-lock="isInModal"
class="emoji-groups"
:class="groupsScrolledClass"
:min-item-size="minItemSize"
:buffer="minItemSize"
:items="emojiItems"
:emit-update="true"
@update="onScroll"
@visible="recalculateItemPerRow"
@resize="recalculateItemPerRow"
>
<template #default="{ item: group, index, active }">
<DynamicScrollerItem
@ -106,6 +118,7 @@
:key="group.id + emoji.displayText"
:title="maybeLocalizedEmojiName(emoji)"
class="emoji-item"
role="button"
@click.stop.prevent="onEmoji(emoji)"
>
<span
@ -116,6 +129,7 @@
v-else
class="emoji-picker-emoji -custom"
loading="lazy"
:alt="maybeLocalizedEmojiName(emoji)"
:src="emoji.imageUrl"
:data-emoji-name="group.id + emoji.displayText"
/>
@ -129,6 +143,17 @@
{{ $t('emoji.keep_open') }}
</Checkbox>
</div>
<div
v-if="!hideCustomEmoji"
class="hide-custom-emoji"
>
<Checkbox
v-model="hideCustomEmojiInPicker"
@change="onShowing"
>
{{ $t('emoji.hide_custom_emoji') }}
</Checkbox>
</div>
</div>
<div
v-if="showingStickers"

View file

@ -1,5 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPlus,
faMinus,
faCheck
} from '@fortawesome/free-solid-svg-icons'
library.add(
faPlus,
faMinus,
faCheck
)
const EMOJI_REACTION_COUNT_CUTOFF = 12
@ -33,6 +45,9 @@ const EmojiReactions = {
},
loggedIn () {
return !!this.$store.state.users.currentUser
},
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
}
},
methods: {
@ -42,10 +57,10 @@ const EmojiReactions = {
reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me
},
fetchEmojiReactionsByIfMissing () {
async fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
return await this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
}
},
reactWith (emoji) {
@ -54,14 +69,26 @@ const EmojiReactions = {
unreact (emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
},
emojiOnClick (emoji, event) {
async emojiOnClick (emoji, event) {
if (!this.loggedIn) return
await this.fetchEmojiReactionsByIfMissing()
if (this.reactedWith(emoji)) {
this.unreact(emoji)
} else {
this.reactWith(emoji)
}
},
counterTriggerAttrs (reaction) {
return {
class: [
'btn',
'button-default',
'emoji-reaction-count-button',
{ '-picked-reaction': this.reactedWith(reaction.name) }
],
'aria-label': this.$tc('status.reaction_count_label', reaction.count, { num: reaction.count })
}
}
}
}

View file

@ -1,20 +1,64 @@
<template>
<div class="EmojiReactions">
<UserListPopover
<span
v-for="(reaction) in emojiReactions"
:key="reaction.name"
:users="accountsForEmoji[reaction.name]"
:key="reaction.url || reaction.name"
class="emoji-reaction-container btn-group"
>
<button
<component
:is="loggedIn ? 'button' : 'a'"
v-bind="!loggedIn ? { href: remoteInteractionLink } : {}"
role="button"
class="emoji-reaction btn button-default"
:class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
:class="{ '-picked-reaction': reactedWith(reaction.name) }"
:title="reaction.url ? reaction.name : undefined"
:aria-pressed="reactedWith(reaction.name)"
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span>
</button>
</UserListPopover>
<span
class="reaction-emoji"
>
<img
v-if="reaction.url"
:src="reaction.url"
class="reaction-emoji-content"
width="1em"
>
<span
v-else
class="reaction-emoji reaction-emoji-content"
>{{ reaction.name }}</span>
</span>
<FALayers>
<FAIcon
v-if="reactedWith(reaction.name)"
class="active-marker"
transform="shrink-6 up-9"
icon="check"
/>
<FAIcon
v-if="!reactedWith(reaction.name)"
class="focus-marker"
transform="shrink-6 up-9"
icon="plus"
/>
<FAIcon
v-else
class="focus-marker"
transform="shrink-6 up-9"
icon="minus"
/>
</FALayers>
</component>
<UserListPopover
:users="accountsForEmoji[reaction.name]"
class="emoji-reaction-popover"
:trigger-attrs="counterTriggerAttrs(reaction)"
@show="fetchEmojiReactionsByIfMissing()"
>
<span class="emoji-reaction-counts">{{ reaction.count }}</span>
</UserListPopover>
</span>
<a
v-if="tooManyReactions"
class="emoji-reaction-expand faint"
@ -28,43 +72,114 @@
<script src="./emoji_reactions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import "../../mixins";
.EmojiReactions {
display: flex;
margin-top: 0.25em;
flex-wrap: wrap;
.emoji-reaction {
padding: 0 0.5em;
margin-right: 0.5em;
--emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1));
.emoji-reaction-container {
display: flex;
align-items: stretch;
margin-top: 0.5em;
margin-right: 0.5em;
.emoji-reaction-popover {
padding: 0;
.emoji-reaction-count-button {
margin: 0;
height: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-sizing: border-box;
min-width: 2em;
display: inline-flex;
justify-content: center;
align-items: center;
&.-picked-reaction {
border: 1px solid var(--accent);
margin-right: -1px;
}
}
}
}
.emoji-reaction {
padding-left: 0.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin: 0;
.reaction-emoji {
width: 1.25em;
width: var(--emoji-size);
height: var(--emoji-size);
margin-right: 0.25em;
line-height: var(--emoji-size);
display: flex;
justify-content: center;
align-items: center;
}
.reaction-emoji-content {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
line-height: inherit;
overflow: hidden;
font-size: calc(var(--emoji-size) * 0.8);
margin: 0;
}
&:focus {
outline: none;
}
&.not-clickable {
cursor: default;
&:hover {
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
}
.svg-inline--fa {
color: var(--text);
}
&.-picked-reaction {
border: 1px solid var(--accent, $fallback--link);
border: 1px solid var(--accent);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
margin-right: -1px;
.svg-inline--fa {
color: var(--accent);
}
}
@include unfocused-style {
.focus-marker {
visibility: hidden;
}
.active-marker {
visibility: visible;
}
}
@include focused-style {
.svg-inline--fa {
color: var(--accent);
}
.focus-marker {
visibility: visible;
}
.active-marker {
visibility: hidden;
}
}
}
@ -75,10 +190,10 @@
display: flex;
align-items: center;
justify-content: center;
&:hover {
text-decoration: underline;
}
}
}
</style>

View file

@ -1,4 +1,7 @@
import Popover from '../popover/popover.vue'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import StatusBookmarkFolderMenu from '../status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH,
@ -32,10 +35,16 @@ library.add(
const ExtraButtons = {
props: ['status'],
components: { Popover },
components: {
Popover,
ConfirmModal,
StatusBookmarkFolderMenu
},
data () {
return {
expanded: false
expanded: false,
showingDeleteDialog: false,
randomSeed: genRandomSeed()
}
},
methods: {
@ -46,11 +55,22 @@ const ExtraButtons = {
this.expanded = false
},
deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm'))
if (confirmed) {
this.$store.dispatch('deleteStatus', { id: this.status.id })
if (this.shouldConfirmDelete) {
this.showDeleteStatusConfirmDialog()
} else {
this.doDeleteStatus()
}
},
doDeleteStatus () {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false
},
pinStatus () {
this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
@ -127,13 +147,28 @@ const ExtraButtons = {
canBookmark () {
return !!this.currentUser
},
bookmarkFolders () {
return this.$store.state.instance.pleromaBookmarkFoldersAvailable
},
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 }
editingAvailable () { return this.$store.state.instance.editingAvailable },
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},
triggerAttrs () {
return {
title: this.$t('status.more_actions'),
id: `popup-trigger-${this.randomSeed}`,
'aria-controls': `popup-menu-${this.randomSeed}`,
'aria-expanded': this.expanded,
'aria-haspopup': 'menu'
}
}
}
}

View file

@ -2,6 +2,7 @@
<Popover
class="ExtraButtons"
trigger="click"
:trigger-attrs="triggerAttrs"
placement="top"
:offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
@ -10,10 +11,15 @@
@close="onClose"
>
<template #content="{close}">
<div class="dropdown-menu">
<div
:id="`popup-menu-${randomSeed}`"
class="dropdown-menu"
role="menu"
>
<button
v-if="canMute && !status.thread_muted"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="muteConversation"
>
<FAIcon
@ -23,7 +29,8 @@
</button>
<button
v-if="canMute && status.thread_muted"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="unmuteConversation"
>
<FAIcon
@ -33,7 +40,8 @@
</button>
<button
v-if="!status.pinned && canPin"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="pinStatus"
@click="close"
>
@ -44,7 +52,8 @@
</button>
<button
v-if="status.pinned && canPin"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="unpinStatus"
@click="close"
>
@ -56,7 +65,8 @@
<template v-if="canBookmark">
<button
v-if="!status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="bookmarkStatus"
@click="close"
>
@ -67,7 +77,8 @@
</button>
<button
v-if="status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="unbookmarkStatus"
@click="close"
>
@ -76,10 +87,15 @@
icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span>
</button>
<StatusBookmarkFolderMenu
v-if="status.bookmarked && bookmarkFolders"
:status="status"
/>
</template>
<button
v-if="ownStatus && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="editStatus"
@click="close"
>
@ -90,7 +106,8 @@
</button>
<button
v-if="isEdited && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="showStatusHistory"
@click="close"
>
@ -101,7 +118,8 @@
</button>
<button
v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="deleteStatus"
@click="close"
>
@ -111,7 +129,8 @@
/><span>{{ $t("status.delete") }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="copyLink"
@click="close"
>
@ -122,7 +141,8 @@
</button>
<a
v-if="!status.is_local"
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
title="Source"
:href="status.external_url"
target="_blank"
@ -133,7 +153,8 @@
/><span>{{ $t("status.external_source") }}</span>
</a>
<button
class="button-default dropdown-item dropdown-item-icon"
class="menu-item dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="reportStatus"
@click="close"
>
@ -165,6 +186,18 @@
/>
</FALayers>
</span>
<teleport to="#modal">
<ConfirmModal
v-if="showingDeleteDialog"
:title="$t('status.delete_confirm_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="hideDeleteStatusConfirmDialog"
@accepted="doDeleteStatus"
>
{{ $t('status.delete_confirm') }}
</ConfirmModal>
</teleport>
</template>
</Popover>
</template>
@ -172,28 +205,23 @@
<script src="./extra_buttons.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import '../../_mixins.scss';
@import "../../mixins";
.ExtraButtons {
/* override of popover internal stuff */
.popover-trigger-button {
width: auto;
}
.popover-trigger {
position: static;
padding: 10px;
margin: -10px;
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
color: var(--text);
}
}
.popover-trigger-button {
/* override of popover internal stuff */
width: auto;
@include unfocused-style {
.focus-marker {
visibility: hidden;

View file

@ -0,0 +1,48 @@
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUserPlus,
faComments,
faBullhorn
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUserPlus,
faComments,
faBullhorn
)
const ExtraNotifications = {
computed: {
shouldShowChats () {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showChatsInExtraNotifications && this.unreadChatCount
},
shouldShowAnnouncements () {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && this.unreadAnnouncementCount
},
shouldShowFollowRequests () {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && this.followRequestCount
},
hasAnythingToShow () {
return this.shouldShowChats || this.shouldShowAnnouncements || this.shouldShowFollowRequests
},
shouldShowCustomizationTip () {
return this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow
},
currentUser () {
return this.$store.state.users.currentUser
},
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'followRequestCount', 'mergedConfig'])
},
methods: {
openNotificationSettings () {
return this.$store.dispatch('openSettingsModalTab', 'notifications')
},
dismissConfigurationTip () {
return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false })
}
}
}
export default ExtraNotifications

View file

@ -0,0 +1,111 @@
<template>
<div class="ExtraNotifications">
<div
v-if="shouldShowChats"
class="notification unseen"
>
<div class="notification-overlay" />
<router-link
class="button-unstyled -link extra-notification"
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 icon"
icon="comments"
/>
{{ $tc('notifications.unread_chats', unreadChatCount, { num: unreadChatCount }) }}
</router-link>
</div>
<div
v-if="shouldShowAnnouncements"
class="notification unseen"
>
<div class="notification-overlay" />
<router-link
class="button-unstyled -link extra-notification"
:to="{ name: 'announcements' }"
>
<FAIcon
fixed-width
class="fa-scale-110 icon"
icon="bullhorn"
/>
{{ $tc('notifications.unread_announcements', unreadAnnouncementCount, { num: unreadAnnouncementCount }) }}
</router-link>
</div>
<div
v-if="shouldShowFollowRequests"
class="notification unseen"
>
<div class="notification-overlay" />
<router-link
class="button-unstyled -link extra-notification"
:to="{ name: 'friend-requests' }"
>
<FAIcon
fixed-width
class="fa-scale-110 icon"
icon="user-plus"
/>
{{ $tc('notifications.unread_follow_requests', followRequestCount, { num: followRequestCount }) }}
</router-link>
</div>
<i18n-t
v-if="shouldShowCustomizationTip"
tag="span"
class="notification tip extra-notification"
keypath="notifications.configuration_tip"
scope="global"
>
<template #theSettings>
<button
class="button-unstyled -link"
@click="openNotificationSettings"
>
{{ $t('notifications.configuration_tip_settings') }}
</button>
</template>
<template #dismiss>
<button
class="button-unstyled -link"
@click="dismissConfigurationTip"
>
{{ $t('notifications.configuration_tip_dismiss') }}
</button>
</template>
</i18n-t>
</div>
</template>
<script src="./extra_notifications.js" />
<style lang="scss">
.ExtraNotifications {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
.notification {
width: 100%;
border-bottom: 1px solid;
border-color: var(--border);
display: flex;
flex-direction: column;
align-items: stretch;
}
.extra-notification {
padding: 1em;
}
.icon {
margin-right: 0.5em;
}
.tip {
display: inline;
}
}
</style>

View file

@ -38,13 +38,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
:title="$t('tool_tip.favorite')"
:href="remoteInteractionLink"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')"
:icon="['far', 'star']"
/>
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
:icon="['far', 'star']"
/>
<FAIcon
class="focus-marker"
transform="shrink-6 up-9 right-12"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
@ -58,8 +65,7 @@
<script src="./favorite_button.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import '../../_mixins.scss';
@import "../../mixins";
.FavoriteButton {
display: flex;
@ -81,8 +87,7 @@
&:hover .svg-inline--fa,
&.-favorited .svg-inline--fa {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
color: var(--cOrange);
}
@include unfocused-style {

View file

@ -2,9 +2,9 @@
<div class="features-panel">
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
<h1 class="title">
{{ $t('features_panel.title') }}
</div>
</h1>
</div>
<div class="panel-body features-panel">
<ul>

View file

@ -42,7 +42,6 @@
<script src="./flash.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.Flash {
display: inline-block;
width: 100%;
@ -78,7 +77,7 @@
.hidden {
display: none;
visibility: 'hidden';
visibility: "hidden";
}
}
</style>

View file

@ -1,12 +1,20 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: {
ConfirmModal
},
data () {
return {
inProgress: false
inProgress: false,
showingConfirmUnfollow: false
}
},
computed: {
shouldConfirmUnfollow () {
return this.$store.getters.mergedConfig.modalOnUnfollow
},
isPressed () {
return this.inProgress || this.relationship.following
},
@ -35,6 +43,12 @@ export default {
}
},
methods: {
showConfirmUnfollow () {
this.showingConfirmUnfollow = true
},
hideConfirmUnfollow () {
this.showingConfirmUnfollow = false
},
onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
},
@ -45,12 +59,21 @@ export default {
})
},
unfollow () {
if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow()
} else {
this.doUnfollow()
}
},
doUnfollow () {
const store = this.$store
this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
})
this.hideConfirmUnfollow()
}
}
}

View file

@ -7,6 +7,28 @@
@click="onClick"
>
{{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmUnfollow"
:title="$t('user_card.unfollow_confirm_title')"
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
:cancel-text="$t('user_card.unfollow_confirm_cancel_button')"
@accepted="doUnfollow"
@cancelled="hideConfirmUnfollow"
>
<i18n-t
scope="global"
keypath="user_card.unfollow_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</button>
</template>

View file

@ -24,6 +24,7 @@
/>
<RemoveFollowerButton
v-if="noFollowsYou && relationship.followed_by"
:user="user"
:relationship="relationship"
class="follow-card-button"
/>
@ -39,9 +40,8 @@
&-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;
}

View file

@ -1,10 +1,18 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = {
props: ['user'],
components: {
BasicUserCard
BasicUserCard,
ConfirmModal
},
data () {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
methods: {
findFollowRequestNotificationId () {
@ -13,7 +21,26 @@ const FollowRequestCard = {
)
return notif && notif.id
},
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
@ -25,14 +52,34 @@ const FollowRequestCard = {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
}
}
}

View file

@ -14,6 +14,28 @@
{{ $t('user_card.deny') }}
</button>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</basic-user-card>
</template>
@ -22,8 +44,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;

View file

@ -1,9 +1,9 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
<h1 class="title">
{{ $t('nav.friend_requests') }}
</div>
</h1>
</div>
<div class="panel-body">
<FollowRequestCard

View file

@ -1,63 +1,59 @@
import { set } from 'lodash'
import Select from '../select/select.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faExclamationTriangle,
faKeyboard,
faFont
} from '@fortawesome/free-solid-svg-icons'
library.add(
faExclamationTriangle,
faKeyboard,
faFont
)
export default {
components: {
Select
Select,
Checkbox,
Popover
},
props: [
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
],
mounted () {
this.$store.dispatch('queryLocalFonts')
},
emits: ['update:modelValue'],
data () {
return {
lValue: this.modelValue,
manualEntry: false,
availableOptions: [
this.noInherit ? '' : 'inherit',
'custom',
...(this.options || []),
'serif',
'sans-serif',
'monospace',
'sans-serif'
...(this.options || [])
].filter(_ => _)
}
},
beforeUpdate () {
this.lValue = this.modelValue
methods: {
toggleManualEntry () {
this.manualEntry = !this.manualEntry
}
},
computed: {
present () {
return typeof this.lValue !== 'undefined'
return typeof this.modelValue !== 'undefined'
},
dValue () {
return this.lValue || this.fallback || {}
localFontsList () {
return this.$store.state.interface.localFonts
},
family: {
get () {
return this.dValue.family
},
set (v) {
set(this.lValue, 'family', v)
this.$emit('update:modelValue', this.lValue)
}
},
isCustom () {
return this.preset === 'custom'
},
preset: {
get () {
if (this.family === 'serif' ||
this.family === 'sans-serif' ||
this.family === 'monospace' ||
this.family === 'inherit') {
return this.family
} else {
return 'custom'
}
},
set (v) {
this.family = v === 'custom' ? '' : v
}
localFontsSize () {
return this.$store.state.interface.localFonts?.length
}
}
}

View file

@ -1,70 +1,144 @@
<template>
<div
class="font-control style-control"
:class="{ custom: isCustom }"
>
<div class="font-control">
<label
:for="preset === 'custom' ? name : name + '-font-switcher'"
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ label }}
</label>
<input
{{ ' ' }}
<Checkbox
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="opt exlcude-disabled"
type="checkbox"
:checked="present"
:model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
<label
v-if="typeof fallback !== 'undefined'"
class="opt-l"
:for="name + '-o'"
/>
{{ ' ' }}
<Select
:id="name + '-font-switcher'"
v-model="preset"
:disabled="!present"
class="font-switcher"
>
<option
v-for="option in availableOptions"
:key="option"
:value="option"
{{ $t('settings.style.themes3.define') }}
</Checkbox>
<p v-if="modelValue?.family">
<label
v-if="manualEntry"
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
</option>
</Select>
<input
v-if="isCustom"
:id="name"
v-model="family"
class="custom-font"
type="text"
>
<i18n-t
keypath="settings.style.themes3.font.entry"
tag="span"
scope="global"
>
<template #fontFamily>
<code>font-family</code>
</template>
</i18n-t>
</label>
<label
v-else
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.select') }}
</label>
{{ ' ' }}
<span
v-if="manualEntry"
class="btn-group"
>
<button
class="btn button-default"
:title="$t('settings.style.themes3.font.lookup_local_fonts')"
@click="toggleManualEntry"
>
<FAIcon
fixed-width
icon="font"
/>
</button>
<input
:id="name"
:model-value="modelValue.family"
class="input custom-font"
type="text"
@update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })"
>
</span>
<span
v-else
class="btn-group"
>
<button
class="btn button-default"
:title="$t('settings.style.themes3.font.enter_manually')"
@click="toggleManualEntry"
>
<FAIcon
fixed-width
icon="keyboard"
/>
</button>
<Select
:id="name + '-local-font-switcher'"
:model-value="modelValue?.family"
class="custom-font"
@update:modelValue="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })"
>
<optgroup
:label="$t('settings.style.themes3.font.group-builtin')"
>
<option
v-for="option in availableOptions"
:key="option"
:value="option"
:style="{ fontFamily: option === 'inherit' ? null : option }"
>
{{ $t('settings.style.themes3.font.builtin.' + option) }}
</option>
</optgroup>
<optgroup
v-if="localFontsSize > 0"
:label="$t('settings.style.themes3.font.group-local')"
>
<option
v-for="option in localFontsList"
:key="option"
:value="option"
:style="{ fontFamily: option }"
>
{{ option }}
</option>
</optgroup>
<optgroup
v-else
:label="$t('settings.style.themes3.font.group-local')"
>
<option disabled>
{{ $t('settings.style.themes3.font.local-unavailable1') }}
</option>
<option disabled>
{{ $t('settings.style.themes3.font.local-unavailable2') }}
</option>
</optgroup>
</Select>
</span>
</p>
</div>
</template>
<script src="./font_control.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.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;
}
.custom-font {
min-width: 20em;
max-width: 20em;
}
}
.invalid-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
}
</style>

View file

@ -0,0 +1,40 @@
export default {
name: 'FunText',
selector: '/*fun-text*/',
virtual: true,
variants: {
greentext: '.greentext',
cyantext: '.cyantext'
},
states: {
faint: '.faint'
},
defaultRules: [
{
directives: {
textColor: '--text',
textAuto: 'preserve'
}
},
{
state: ['faint'],
directives: {
textOpacity: 0.5
}
},
{
variant: 'greentext',
directives: {
textColor: '--cGreen',
textAuto: 'preserve'
}
},
{
variant: 'cyantext',
directives: {
textColor: '--cBlue',
textAuto: 'preserve'
}
}
]
}

View file

@ -4,6 +4,7 @@ import { sumBy, set } from 'lodash'
const Gallery = {
props: [
'attachments',
'compact',
'limitRows',
'descriptions',
'limit',

View file

@ -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,8 +87,6 @@
<script src='./gallery.js'></script>
<style lang="scss">
@import '../../_variables.scss';
.Gallery {
.gallery-rows {
display: flex;
@ -100,6 +99,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 +160,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 +184,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>

View file

@ -4,7 +4,7 @@
v-for="(notice, index) in notices"
:key="index"
class="alert global-notice"
:class="{ ['global-' + notice.level]: true }"
:class="{ [notice.level]: true }"
>
<div class="notice-message">
{{ $t(notice.messageKey, notice.messageArgs) }}
@ -25,14 +25,12 @@
<script src="./global_notice_list.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.global-notice-list {
position: fixed;
top: calc(var(--navbar-height) + 0.5em);
width: 100%;
pointer-events: none;
z-index: var(--ZI_navbar_popovers);
z-index: var(--ZI_modals_popovers);
display: flex;
flex-direction: column;
align-items: center;
@ -52,45 +50,8 @@
}
}
.global-error {
background-color: var(--alertPopupError, $fallback--cRed);
color: var(--alertPopupErrorText, $fallback--text);
.svg-inline--fa {
color: var(--alertPopupErrorText, $fallback--text);
}
}
.global-warning {
background-color: var(--alertPopupWarning, $fallback--cOrange);
color: var(--alertPopupWarningText, $fallback--text);
.svg-inline--fa {
color: var(--alertPopupWarningText, $fallback--text);
}
}
.global-success {
background-color: var(--alertPopupSuccess, $fallback--cGreen);
color: var(--alertPopupSuccessText, $fallback--text);
.svg-inline--fa {
color: var(--alertPopupSuccessText, $fallback--text);
}
}
.global-info {
background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text);
.svg-inline--fa {
color: var(--alertPopupNeutralText, $fallback--text);
}
}
.close-notice {
padding-right: 0.2em;
.svg-inline--fa:hover {
opacity: 0.6;
}
}
}
</style>

View file

@ -0,0 +1,14 @@
export default {
name: 'Icon',
virtual: true,
selector: '.svg-inline--fa',
defaultRules: [
{
component: 'Icon',
directives: {
textColor: '$blend(--stack 0.5 --parent--text)',
textAuto: 'no-auto'
}
}
]
}

View file

@ -41,7 +41,7 @@
<input
ref="input"
type="file"
class="image-cropper-img-input"
class="input image-cropper-img-input"
:accept="mimes"
>
</div>

View file

@ -3,6 +3,7 @@
<form>
<input
ref="input"
class="input"
type="file"
@change="change"
>

View file

@ -0,0 +1,94 @@
export default {
name: 'Input',
selector: '.input',
states: {
hover: ':hover:not(.disabled)',
focused: ':focus-within',
disabled: '.disabled'
},
variants: {
checkbox: '.-checkbox',
radio: '.-radio'
},
validInnerComponents: [
'Text',
'Icon'
],
defaultRules: [
{
component: 'Root',
directives: {
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2)',
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
}
},
{
variant: 'checkbox',
directives: {
roundness: 1
}
},
{
directives: {
'--font': 'generic | inherit',
background: '--fg, -5',
roundness: 3,
shadow: [{
x: 0,
y: 0,
blur: 2,
spread: 0,
color: '#000000',
alpha: 1
}, '--defaultInputBevel']
}
},
{
state: ['hover'],
directives: {
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel']
}
},
{
state: ['focused'],
directives: {
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel']
}
},
{
state: ['focused', 'hover'],
directives: {
shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel']
}
},
{
state: ['disabled'],
directives: {
background: '--parent'
}
},
{
component: 'Text',
parent: {
component: 'Input',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'Input',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]
}

View file

@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
const tabModeDict = {
mentions: ['mention'],
statuses: ['status'],
'likes+repeats': ['repeat', 'like'],
follows: ['follow'],
reactions: ['pleroma:emoji_reaction'],

View file

@ -1,18 +1,22 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">
<h1 class="title">
{{ $t("nav.interactions") }}
</div>
</h1>
</div>
<tab-switcher
ref="tabSwitcher"
:on-switch="onModeSwitch"
>
<span
key="mentions"
key="statuses"
:label="$t('nav.mentions')"
/>
<span
key="statuses"
:label="$t('interactions.statuses')"
/>
<span
key="likes+repeats"
:label="$t('interactions.favs_repeats')"
@ -39,6 +43,7 @@
<Notifications
ref="notifications"
:no-heading="true"
:no-extra="true"
:minimal-mode="true"
:filter-mode="filterMode"
/>

View file

@ -1,21 +1,46 @@
<template>
<div>
<label for="interface-language-switcher">
<div class="interface-language-switcher">
<label>
{{ promptText }}
</label>
{{ ' ' }}
<Select
id="interface-language-switcher"
v-model="controlledLanguage"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
<ul class="setting-list">
<li
v-for="index of controlledLanguage.keys()"
:key="index"
>
{{ lang.name }}
</option>
</Select>
<label>
{{ index === 0 ? $t('settings.primary_language') : $tc('settings.fallback_language', index, { index }) }}
<Select
class="language-select"
:model-value="controlledLanguage[index]"
@update:modelValue="val => setLanguageAt(index, val)"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
{{ lang.name }}
</option>
</Select>
</label>
<button
v-if="controlledLanguage.length > 1 && index !== 0"
class="button-default btn"
@click="() => removeLanguageAt(index)"
>
{{ $t('settings.remove_language') }}
</button>
</li>
<li>
<button
class="button-default btn"
@click="addLanguage"
>
{{ $t('settings.add_language') }}
</button>
</li>
</ul>
</div>
</template>
@ -34,7 +59,7 @@ export default {
required: true
},
language: {
type: String,
type: [Array, String],
required: true
},
setLanguage: {
@ -48,7 +73,9 @@ export default {
},
controlledLanguage: {
get: function () { return this.language },
get: function () {
return Array.isArray(this.language) ? this.language : [this.language]
},
set: function (val) {
this.setLanguage(val)
}
@ -58,7 +85,28 @@ export default {
methods: {
getLanguageName (code) {
return localeService.getLanguageName(code)
},
addLanguage () {
this.controlledLanguage = [...this.controlledLanguage, '']
},
setLanguageAt (index, val) {
const lang = [...this.controlledLanguage]
lang[index] = val
this.controlledLanguage = lang
},
removeLanguageAt (index) {
const lang = [...this.controlledLanguage]
lang.splice(index, 1)
this.controlledLanguage = lang
}
}
}
</script>
<style lang="scss">
.interface-language-switcher {
.language-select {
margin-right: 1em;
}
}
</style>

View file

@ -33,8 +33,6 @@
<script src="./link-preview.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.link-preview-card {
display: flex;
flex-direction: row;
@ -46,12 +44,12 @@
flex-shrink: 0;
width: 120px;
max-width: 25%;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-radius: var(--roundness);
}
}
@ -67,7 +65,7 @@
}
.card-description {
margin: 0.5em 0 0 0;
margin: 0.5em 0 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
@ -81,13 +79,10 @@
margin: 2em 0;
}
color: $fallback--text;
color: var(--text, $fallback--text);
color: var(--text);
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-radius: var(--roundness);
border-color: var(--border);
}
</style>

View file

@ -0,0 +1,24 @@
export default {
name: 'Link',
selector: 'a',
virtual: true,
states: {
faint: '.faint'
},
defaultRules: [
{
component: 'Link',
directives: {
textColor: '--link'
}
},
{
component: 'Link',
state: ['faint'],
directives: {
textOpacity: 0.5,
textOpacityMode: 'fake'
}
}
]
}

View file

@ -1,9 +1,14 @@
<template>
<div class="list">
<div
class="list"
role="list"
>
<div
v-for="item in items"
:key="getKey(item)"
class="list-item"
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
role="listitem"
>
<slot
name="item"
@ -29,24 +34,15 @@ export default {
getKey: {
type: Function,
default: item => item.id
},
getClass: {
type: Function,
default: item => ''
},
nonInteractive: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="scss">
@import '../../_variables.scss';
.list {
&-item:not(:last-child) {
border-bottom: 1px solid;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
&-empty-content {
text-align: center;
padding: 10px;
}
}
</style>

View file

@ -0,0 +1,48 @@
export default {
name: 'ListItem',
selector: '.list-item',
states: {
active: '.-active',
hover: ':hover:not(.-non-interactive)'
},
validInnerComponents: [
'Text',
'Link',
'Icon',
'Border',
'Button',
'ButtonUnstyled',
'RichContent',
'Input',
'Avatar'
],
defaultRules: [
{
directives: {
background: '--bg',
opacity: 0
}
},
{
state: ['active'],
directives: {
background: '--inheritedBackground, 10',
opacity: 1
}
},
{
state: ['hover'],
directives: {
background: '--inheritedBackground, 10',
opacity: 1
}
},
{
state: ['hover', 'active'],
directives: {
background: '--inheritedBackground, 20',
opacity: 1
}
}
]
}

View file

@ -2,7 +2,9 @@
<div class="Lists panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('lists.lists') }}
<h1 class="title">
{{ $t('lists.lists') }}
</h1>
</div>
<router-link
:to="{ name: 'lists-new' }"

View file

@ -21,31 +21,18 @@
<script src="./lists_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.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);
}
}
.list-name {
flex-grow: 1;
color: var(--link);
}
</style>

View file

@ -17,6 +17,7 @@
<i18n-t
v-if="id"
keypath="lists.editing_list"
scope="global"
>
<template #listTitle>
{{ title }}
@ -25,6 +26,7 @@
<i18n-t
v-else
keypath="lists.creating_list"
scope="global"
/>
</div>
</div>
@ -36,6 +38,7 @@
id="list-edit-title"
ref="title"
v-model="titleDraft"
class="input"
>
<button
v-if="id"
@ -164,8 +167,6 @@
<script src="./lists_edit.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.ListEdit {
--panel-body-padding: 0.5em;

View file

@ -10,6 +10,7 @@
<input
ref="search"
v-model="query"
class="input"
:placeholder="$t('lists.search')"
@input="onInput"
>
@ -27,12 +28,10 @@
<script src="./lists_user_search.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.ListsUserSearch {
.input-wrap {
display: flex;
margin: 0.7em 0.5em 0.7em 0.5em;
margin: 0.7em 0.5em;
input {
width: 100%;

View file

@ -3,7 +3,9 @@
<!-- Default panel contents -->
<div class="panel-heading">
{{ $t('login.login') }}
<h1 class="title">
{{ $t('login.login') }}
</h1>
</div>
<div class="panel-body">
@ -18,7 +20,7 @@
id="username"
v-model="user.username"
:disabled="loggingIn"
class="form-control"
class="input form-control"
:placeholder="$t('login.placeholder')"
>
</div>
@ -29,7 +31,7 @@
ref="passwordInput"
v-model="user.password"
:disabled="loggingIn"
class="form-control"
class="input form-control"
type="password"
>
</div>
@ -93,8 +95,6 @@
<script src="./login_form.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.login-form {
display: flex;
flex-direction: column;
@ -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;

View file

@ -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: {

Some files were not shown because too many files have changed in this diff Show more