Merge branch 'develop' into fedi-absturztau-be

This commit is contained in:
Absturztaube 2020-08-27 21:41:14 +02:00
commit 07853ed322
48 changed files with 2662 additions and 715 deletions

View File

@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
image: node:8
image: node:10
stages:
- lint
@ -14,6 +14,7 @@ lint:
script:
- yarn
- npm run lint
- npm run stylelint
test:
stage: test

19
.stylelintrc.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": [
"stylelint-rscss/config",
"stylelint-config-recommended",
"stylelint-config-standard"
],
"rules": {
"declaration-no-important": true,
"rscss/no-descendant-combinator": false,
"rscss/class-format": [
true,
{
"component": "pascal-case",
"variant": "^-[a-z]\\w+",
"element": "^[a-z]\\w+"
}
]
}
}

View File

@ -2,7 +2,7 @@
> A single column frontend designed for Pleroma.
![screenshot](https://i.imgur.com/DJVqSJ0.png)
![screenshot](/uploads/796c5ecf985ed1e2b0943ee0df131ed0/DJVqSJ0.png)
# For Translators

View File

@ -11,6 +11,7 @@
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"stylelint": "npx stylelint src/components/status/status.scss",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
},
@ -22,8 +23,8 @@
"cropperjs": "^1.4.3",
"diff": "^3.0.1",
"escape-html": "^1.0.3",
"parse-link-header": "^1.0.1",
"localforage": "^1.5.0",
"parse-link-header": "^1.0.1",
"phoenix": "^1.3.0",
"portal-vue": "^2.1.4",
"v-click-outside": "^2.1.1",
@ -36,7 +37,6 @@
"vuex": "^3.0.1"
},
"devDependencies": {
"karma-mocha-reporter": "^2.2.1",
"@babel/core": "^7.7.5",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
@ -80,6 +80,7 @@
"karma-coverage": "^1.1.1",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.2.0",
"karma-mocha-reporter": "^2.2.1",
"karma-sinon-chai": "^2.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26",
@ -101,6 +102,9 @@
"shelljs": "^0.7.4",
"sinon": "^2.1.0",
"sinon-chai": "^2.8.0",
"stylelint": "^13.6.1",
"stylelint-config-standard": "^20.0.0",
"stylelint-rscss": "^0.4.0",
"url-loader": "^1.1.2",
"vue-loader": "^14.0.0",
"vue-style-loader": "^4.0.0",

View File

@ -60,6 +60,7 @@
@click="openModal"
>
<StillImage
class="image"
:referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
@ -281,8 +282,11 @@
}
.image-attachment {
width: 100%;
height: 100%;
&,
& .image {
width: 100%;
height: 100%;
}
&.hidden {
display: none;

View File

@ -72,12 +72,12 @@
}
}
.avatar.still-image {
.Avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
.status-body {
.StatusContent {
img.emoji {
width: 1.4em;
height: 1.4em;

View File

@ -2,7 +2,7 @@
.chat-message-wrapper {
&.hovered-message-chain {
.animated.avatar {
.animated.Avatar {
canvas {
display: none;
}

View File

@ -51,7 +51,7 @@
}
}
.still-image.avatar {
.Avatar {
width: 23px;
height: 23px;
margin-right: 0.5em;

View File

@ -1,7 +1,7 @@
<template>
<div
class="timeline panel-default"
:class="[isExpanded ? 'panel' : 'panel-disabled']"
class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
>
<div
v-if="isExpanded"
@ -28,7 +28,7 @@
:replies="getReplies(status.id)"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="status-fadein panel-body"
class="conversation-status status-fadein panel-body"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
/>
@ -40,14 +40,27 @@
<style lang="scss">
@import '../../_variables.scss';
.timeline {
.panel-disabled {
.status-el {
border-left: none;
border-bottom-width: 1px;
border-bottom-style: solid;
.Conversation {
.conversation-status {
border-left: none;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border);
border-radius: 0;
}
&.-expanded {
.conversation-status {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-radius: 0;
border-left: 4px solid $fallback--cRed;
border-left: 4px solid var(--cRed, $fallback--cRed);
}
.conversation-status:last-child {
border-bottom: none;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
}
}
}

View File

@ -1,3 +1,4 @@
import { timelineNames } from '../timeline_menu/timeline_menu.js'
import { mapState, mapGetters } from 'vuex'
const NavPanel = {
@ -7,9 +8,17 @@ const NavPanel = {
}
},
computed: {
onTimelineRoute () {
return !!timelineNames()[this.$route.name]
},
timelinesRoute () {
if (this.$store.state.interface.lastTimeline) {
return this.$store.state.interface.lastTimeline
}
return this.currentUser ? 'friends' : 'public-timeline'
},
...mapState({
currentUser: state => state.users.currentUser,
chat: state => state.chat.channel,
followRequestCount: state => state.api.followRequests.length,
privateMode: state => state.instance.private,
federating: state => state.instance.federating,

View File

@ -2,9 +2,12 @@
<div class="nav-panel">
<div class="panel panel-default">
<ul>
<li v-if="currentUser">
<router-link :to="{ name: 'friends' }">
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
<li v-if="currentUser || !privateMode">
<router-link
:to="{ name: timelinesRoute }"
:class="onTimelineRoute && 'router-link-active'"
>
<i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }}
</router-link>
</li>
<li v-if="currentUser">
@ -12,16 +15,6 @@
<i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'bookmarks'}">
<i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
</router-link>
</li>
<li v-if="currentUser && pleromaChatMessagesAvailable">
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
<div
@ -44,16 +37,6 @@
</span>
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link :to="{ name: 'public-timeline' }">
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'about' }">
<i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}

View File

@ -0,0 +1,52 @@
// TODO Copypaste from Status, should unify it somehow
.Notification {
&.-muted {
padding: 0.25em 0.6em;
height: 1.2em;
line-height: 1.2em;
text-overflow: ellipsis;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
& .status-username,
& .mute-thread,
& .mute-words {
word-wrap: normal;
word-break: normal;
white-space: nowrap;
}
& .status-username,
& .mute-words {
text-overflow: ellipsis;
overflow: hidden;
}
.status-username {
font-weight: normal;
flex: 0 1 auto;
margin-right: 0.2em;
font-size: smaller;
}
.mute-thread {
flex: 0 0 auto;
}
.mute-words {
flex: 1 0 5em;
margin-left: 0.2em;
&::before {
content: ' ';
}
}
.unmute {
flex: 0 0 auto;
margin-left: auto;
display: block;
}
}
}

View File

@ -7,7 +7,7 @@
<div v-else>
<div
v-if="needMute && !unmuted"
class="container muted"
class="Notification container -muted"
>
<small>
<router-link :to="userProfileLink">
@ -171,3 +171,4 @@
</template>
<script src="./notification.js"></script>
<style src="./notification.scss" lang="scss"></style>

View File

@ -39,7 +39,7 @@
word-wrap: break-word;
word-break: break-word;
&:hover .animated.avatar {
&:hover .animated.Avatar {
canvas {
display: none;
}
@ -60,16 +60,8 @@
height: 32px;
}
.status-body {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
a {
color: var(--faintLink);
}
.status-content a {
color: var(--postFaintLink);
}
}
--link: var(--faintLink);
--text: var(--faint);
}
.follow-request-accept {
@ -106,7 +98,8 @@
}
}
.status-el {
/* TODO cleanup this */
.Status {
flex: 1;
}

View File

@ -17,7 +17,7 @@
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<span v-html="option.title_html"></span>
<span v-html="option.title_html" />
</div>
<div
class="result-fill"
@ -96,6 +96,7 @@
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
word-break: break-word;
}
.result-percentage {
width: 3.5em;

View File

@ -75,6 +75,7 @@ export default {
deleteOption (index, event) {
if (this.options.length > 2) {
this.options.splice(index, 1)
this.updatePollToParent()
}
},
convertExpiryToUnit (unit, amount) {

View File

@ -18,7 +18,9 @@ const Popover = {
// Takes a x/y object and tells how many pixels to offset from
// anchor point on either axis
offset: Object,
// Additional styles you may want for the popover container
// Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover
// styles with your custom class.
popoverClass: String
},
data () {
@ -106,7 +108,7 @@ const Popover = {
// single translate or translate3d resulted in blurry text.
this.styles = {
opacity: 1,
transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
}
},
showPopover () {

View File

@ -14,7 +14,7 @@
ref="content"
:style="styles"
class="popover"
:class="popoverClass"
:class="popoverClass || 'popover-default'"
>
<slot
name="content"
@ -34,6 +34,9 @@
z-index: 8;
position: absolute;
min-width: 0;
}
.popover-default {
transition: opacity 0.3s;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);

View File

@ -79,10 +79,7 @@
@click.stop.prevent="togglePreview"
>
{{ $t('post_status.preview') }}
<i
class="icon-down-open"
:style="{ transform: showPreview ? 'rotate(0deg)' : 'rotate(-90deg)' }"
/>
<i :class="showPreview ? 'icon-left-open' : 'icon-right-open'" />
</a>
<i
v-show="previewLoading"
@ -146,6 +143,7 @@
v-model="newStatus.status"
:placeholder="placeholder || $t('post_status.default')"
rows="1"
cols="1"
:disabled="posting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }"
@ -374,6 +372,7 @@
}
.preview-heading {
padding-left: 0.5em;
display: flex;
width: 100%;
@ -385,14 +384,16 @@
.preview-toggle {
display: flex;
cursor: pointer;
user-select: none;
&:hover {
text-decoration: underline;
}
}
.icon-down-open {
transition: transform 0.1s;
i {
margin-left: 0.2em;
font-size: 0.8em;
transform: rotate(90deg);
}
}
.preview-container {

View File

@ -13,6 +13,13 @@
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
*/
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
@media all and (max-width: 800px) {
/* For mobile, the modal takes 100% of the available screen.
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
*/
transform: translateY(calc(100% - 50px));
}
}
}
@ -27,7 +34,7 @@
@media all and (max-width: 800px) {
max-width: 100vw;
height: 100vh;
height: 100%;
}
>.panel-body {

View File

@ -49,6 +49,12 @@ const SideDrawer = {
federating () {
return this.$store.state.instance.federating
},
timelinesRoute () {
if (this.$store.state.interface.lastTimeline) {
return this.$store.state.interface.lastTimeline
}
return this.currentUser ? 'friends' : 'public-timeline'
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
}),

View File

@ -39,13 +39,18 @@
<i class="button-icon icon-login" /> {{ $t("login.login") }}
</router-link>
</li>
<li
v-if="currentUser || !privateMode"
@click="toggleDrawer"
>
<router-link :to="{ name: timelinesRoute }">
<i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }}
</router-link>
</li>
<li
v-if="currentUser && pleromaChatMessagesAvailable"
@click="toggleDrawer"
>
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
</router-link>
<router-link
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
style="position: relative"
@ -59,34 +64,15 @@
</span>
</router-link>
</li>
<li
v-if="currentUser"
@click="toggleDrawer"
>
</ul>
<ul v-if="currentUser">
<li @click="toggleDrawer">
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
<i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
</router-link>
</li>
</ul>
<ul>
<li
v-if="currentUser"
@click="toggleDrawer"
>
<router-link :to="{ name: 'friends' }">
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
</router-link>
</li>
<li
v-if="currentUser"
@click="toggleDrawer"
>
<router-link :to="{ name: 'bookmarks'}">
<i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
</router-link>
</li>
<li
v-if="currentUser && currentUser.locked"
v-if="currentUser.locked"
@click="toggleDrawer"
>
<router-link to="/friend-requests">
@ -100,19 +86,11 @@
</router-link>
</li>
<li
v-if="currentUser || !privateMode"
v-if="chat"
@click="toggleDrawer"
>
<router-link to="/main/public">
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
<li
v-if="federating && (currentUser || !privateMode)"
@click="toggleDrawer"
>
<router-link to="/main/all">
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
<router-link :to="{ name: 'chat' }">
<i class="button-icon icon-chat" /> {{ $t("nav.chat") }}
</router-link>
</li>
</ul>

View File

@ -0,0 +1,414 @@
@import '../../_variables.scss';
$status-margin: 0.75em;
.Status {
min-width: 0;
&:hover {
--still-image-img: visible;
--still-image-canvas: hidden;
}
&.-focused {
background-color: $fallback--lightBg;
background-color: var(--selectedPost, $fallback--lightBg);
color: $fallback--text;
color: var(--selectedPostText, $fallback--text);
--lightText: var(--selectedPostLightText, $fallback--light);
--faint: var(--selectedPostFaintText, $fallback--faint);
--faintLink: var(--selectedPostFaintLink, $fallback--faint);
--postLink: var(--selectedPostPostLink, $fallback--faint);
--postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
--icon: var(--selectedPostIcon, $fallback--icon);
}
.status-container {
display: flex;
padding: $status-margin;
&.-repeat {
padding-top: 0;
}
}
.pin {
padding: $status-margin $status-margin 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.left-side {
margin-right: $status-margin;
}
.right-side {
flex: 1;
min-width: 0;
}
.usercard {
margin-bottom: $status-margin;
}
.status-username {
white-space: nowrap;
font-size: 14px;
overflow: hidden;
max-width: 85%;
font-weight: bold;
flex-shrink: 1;
margin-right: 0.4em;
text-overflow: ellipsis;
.emoji {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain;
}
}
.status-favicon {
height: 18px;
width: 18px;
margin-right: 0.4em;
}
.status-heading {
margin-bottom: 0.5em;
}
.heading-name-row {
display: flex;
justify-content: space-between;
line-height: 18px;
a {
display: inline-block;
word-break: break-all;
}
}
.account-name {
min-width: 1.6em;
margin-right: 0.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 0;
}
.heading-left {
display: flex;
min-width: 0;
}
.heading-right {
display: flex;
flex-shrink: 0;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
position: relative;
align-content: baseline;
font-size: 12px;
line-height: 18px;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
}
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
max-width: 100%;
.reply-to-link {
white-space: nowrap;
word-break: break-word;
text-overflow: ellipsis;
overflow-x: hidden;
}
.icon-reply {
// mirror the icon
transform: scaleX(-1);
}
}
& .reply-to-popover,
& .reply-to-no-popover {
min-width: 0;
margin-right: 0.4em;
flex-shrink: 0;
}
.reply-to-popover {
.reply-to:hover::before {
content: '';
display: block;
position: absolute;
bottom: 0;
width: 100%;
border-bottom: 1px solid var(--faint);
pointer-events: none;
}
.faint-link:hover {
// override default
text-decoration: none;
}
&.-strikethrough {
.reply-to::after {
content: '';
display: block;
position: absolute;
top: 50%;
width: 100%;
border-bottom: 1px solid var(--faint);
pointer-events: none;
}
}
}
.reply-to {
display: flex;
position: relative;
}
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 0.2em;
}
.replies-separator {
margin-left: 0.4em;
}
.replies {
line-height: 18px;
font-size: 12px;
display: flex;
flex-wrap: wrap;
& > * {
margin-right: 0.4em;
}
}
.reply-link {
height: 17px;
}
.repeat-info {
padding: 0.4em $status-margin;
line-height: 22px;
.right-side {
display: flex;
align-content: center;
flex-wrap: wrap;
}
i {
padding: 0 0.2em;
}
}
.repeater-avatar {
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
width: 20px;
height: 20px;
}
.repeater-name {
text-overflow: ellipsis;
margin-right: 0;
.emoji {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain;
}
}
.status-fadein {
animation-duration: 0.4s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.status-actions {
position: relative;
width: 100%;
display: flex;
margin-top: $status-margin;
> * {
max-width: 4em;
flex: 1;
}
}
.button-reply {
&:not(.-disabled) {
cursor: pointer;
}
&:not(.-disabled):hover,
&.-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
.muted {
padding: 0.25em 0.6em;
height: 1.2em;
line-height: 1.2em;
text-overflow: ellipsis;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
& .status-username,
& .mute-thread,
& .mute-words {
word-wrap: normal;
word-break: normal;
white-space: nowrap;
}
& .status-username,
& .mute-words {
text-overflow: ellipsis;
overflow: hidden;
}
.status-username {
font-weight: normal;
flex: 0 1 auto;
margin-right: 0.2em;
font-size: smaller;
}
.mute-thread {
flex: 0 0 auto;
}
.mute-words {
flex: 1 0 5em;
margin-left: 0.2em;
&::before {
content: ' ';
}
}
.unmute {
flex: 0 0 auto;
margin-left: auto;
display: block;
}
}
.reply-form {
padding-top: 0;
padding-bottom: 0;
}
.reply-body {
flex: 1;
}
.favs-repeated-users {
margin-top: $status-margin;
}
.stats {
width: 100%;
display: flex;
line-height: 1em;
}
.avatar-row {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
height: 100%;
width: 1px;
left: 0;
background-color: var(--faint, $fallback--faint);
}
}
.stat-count {
margin-right: $status-margin;
user-select: none;
.stat-title {
color: var(--faint, $fallback--faint);
font-size: 12px;
text-transform: uppercase;
position: relative;
}
.stat-number {
font-weight: bolder;
font-size: 16px;
line-height: 1em;
}
&:hover .stat-title {
text-decoration: underline;
}
}
@media all and (max-width: 800px) {
.repeater-avatar {
margin-left: 20px;
}
.avatar:not(.repeater-avatar) {
width: 40px;
height: 40px;
// TODO define those other way somehow?
// stylelint-disable rscss/class-format
&.avatar-compact {
width: 32px;
height: 32px;
}
}
}
}

View File

@ -2,8 +2,8 @@
<!-- eslint-disable vue/no-v-html -->
<div
v-if="!hideStatus"
class="status-el"
:class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"
class="Status"
:class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]"
>
<div
v-if="error"
@ -16,8 +16,8 @@
/>
</div>
<template v-if="muted && !isPreview">
<div class="media status container muted">
<small class="username">
<div class="status-csontainer muted">
<small class="status-username">
<i
v-if="muted && retweet"
class="button-icon icon-retweet"
@ -54,7 +54,7 @@
<template v-else>
<div
v-if="showPinned"
class="status-pin"
class="pin"
>
<i class="fa icon-pin faint" />
<span class="faint">{{ $t('status.pinned') }}</span>
@ -63,17 +63,17 @@
v-if="retweet && !noHeading && !inConversation"
:class="[repeaterClass, { highlighted: repeaterStyle }]"
:style="[repeaterStyle]"
class="media container retweet-info"
class="status-container repeat-info"
>
<UserAvatar
v-if="retweet"
class="media-left"
class="left-side repeater-avatar"
:better-shadow="betterShadow"
:user="statusoid.user"
/>
<div class="media-body faint">
<div class="right-side faint">
<span
class="user-name"
class="status-username repeater-name"
:title="retweeter"
>
<router-link
@ -95,14 +95,14 @@
</div>
<div
:class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]"
:class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]"
:style="[ userStyle ]"
class="media status"
class="status-container"
:data-tags="tags"
>
<div
v-if="!noHeading"
class="media-left"
class="left-side"
>
<router-link
:to="userProfileLink"
@ -115,29 +115,29 @@
/>
</router-link>
</div>
<div class="status-body">
<div class="right-side">
<UserCard
v-if="userExpanded"
:user-id="status.user.id"
:rounded="true"
:bordered="true"
class="status-usercard"
class="usercard"
/>
<div
v-if="!noHeading"
class="media-heading"
class="status-heading"
>
<div class="heading-name-row">
<div class="name-and-account-name">
<div class="heading-left">
<h4
v-if="status.user.name_html"
class="user-name"
class="status-username"
:title="status.user.name"
v-html="status.user.name_html"
/>
<h4
v-else
class="user-name"
class="status-username"
:title="status.user.name"
>
{{ status.user.name }}
@ -150,8 +150,8 @@
{{ status.user.screen_name }}
</router-link>
<img
class="status-favicon"
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
:src="status.user.favicon"
>
</div>
@ -211,6 +211,7 @@
:status-id="status.parent_visible && status.in_reply_to_status_id"
class="reply-to-popover"
style="min-width: 0"
:class="{ '-strikethrough': !status.parent_visible }"
>
<a
class="reply-to"
@ -218,10 +219,9 @@
:aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
>
<i class="button-icon icon-reply" />
<i class="button-icon reply-button icon-reply" />
<span
class="faint-link reply-to-text"
:class="{ 'strikethrough': !status.parent_visible }"
>
{{ $t('status.reply_to') }}
</span>
@ -229,11 +229,12 @@
</StatusPopover>
<span
v-else
class="reply-to"
class="reply-to-no-popover"
>
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
</span>
<router-link
class="reply-to-link"
:title="replyToName"
:to="replyProfileLink"
>
@ -317,19 +318,19 @@
<div
v-if="!noHeading && !isPreview"
class="status-actions media-body"
class="status-actions"
>
<div>
<i
v-if="loggedIn"
class="button-icon icon-reply"
class="button-icon button-reply icon-reply"
:title="$t('tool_tip.reply')"
:class="{'button-icon-active': replying}"
:class="{'-active': replying}"
@click.prevent="toggleReplying"
/>
<i
v-else
class="button-icon button-icon-disabled icon-reply"
class="button-icon button-reply -disabled icon-reply"
:title="$t('tool_tip.reply')"
/>
<span v-if="status.replies_count > 0">{{ status.replies_count }}</span>
@ -357,7 +358,7 @@
</div>
<div
v-if="replying"
class="container"
class="status-container reply-form"
>
<PostStatusForm
class="reply-body"
@ -375,439 +376,4 @@
</template>
<script src="./status.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
$status-margin: 0.75em;
.status-body {
flex: 1;
min-width: 0;
}
.status-pin {
padding: $status-margin $status-margin 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.media-left {
margin-right: $status-margin;
}
.status-el {
border-left-width: 0px;
min-width: 0;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left: 4px $fallback--cRed;
border-left: 4px var(--cRed, $fallback--cRed);
&_focused {
background-color: $fallback--lightBg;
background-color: var(--selectedPost, $fallback--lightBg);
color: $fallback--text;
color: var(--selectedPostText, $fallback--text);
--lightText: var(--selectedPostLightText, $fallback--light);
--faint: var(--selectedPostFaintText, $fallback--faint);
--faintLink: var(--selectedPostFaintLink, $fallback--faint);
--postLink: var(--selectedPostPostLink, $fallback--faint);
--postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
--icon: var(--selectedPostIcon, $fallback--icon);
}
.timeline & {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.media-body {
flex: 1;
padding: 0;
}
.status-usercard {
margin-bottom: $status-margin;
}
.user-name {
white-space: nowrap;
font-size: 14px;
overflow: hidden;
flex-shrink: 0;
max-width: 85%;
font-weight: bold;
img.emoji {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
.status-favicon {
height: 18px;
width: 18px;
margin-right: 0.4em;
}
.media-heading {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
margin-bottom: 0.5em;
small {
font-weight: lighter;
}
.heading-name-row {
padding: 0;
display: flex;
justify-content: space-between;
line-height: 18px;
a {
display: inline-block;
word-break: break-all;
}
.name-and-account-name {
display: flex;
min-width: 0;
}
.user-name {
flex-shrink: 1;
margin-right: 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
.account-name {
min-width: 1.6em;
margin-right: 0.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 0;
}
}
.heading-right {
display: flex;
flex-shrink: 0;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
position: relative;
align-content: baseline;
font-size: 12px;
line-height: 18px;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
> .reply-to-and-accountname > a {
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
}
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
}
}
.reply-info {
display: flex;
}
.reply-to-popover {
min-width: 0;
}
.reply-to {
display: flex;
}
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.4em 0 0.2em;
}
.strikethrough {
text-decoration: line-through;
}
.replies-separator {
margin-left: 0.4em;
}
.replies {
line-height: 18px;
font-size: 12px;
display: flex;
flex-wrap: wrap;
& > * {
margin-right: 0.4em;
}
}
.reply-link {
height: 17px;
}
}
.retweet-info {
padding: 0.4em $status-margin;
margin: 0;
.avatar.still-image {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
width: 20px;
height: 20px;
}
.media-body {
font-size: 1em;
line-height: 22px;
display: flex;
align-content: center;
flex-wrap: wrap;
.user-name {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
i {
padding: 0 0.2em;
}
a {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.status-fadein {
animation-duration: 0.4s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.status-conversation {
border-left-style: solid;
}
.status-actions {
position: relative;
width: 100%;
display: flex;
margin-top: $status-margin;
> * {
max-width: 4em;
flex: 1;
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled):hover,
&.button-icon-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
cursor: pointer;
}
}
.status:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
.status {
display: flex;
padding: $status-margin;
&.is-retweet {
padding-top: 0;
}
}
.status-conversation:last-child {
border-bottom: none;
}
.muted {
padding: .25em .6em;
height: 1.2em;
line-height: 1.2em;
text-overflow: ellipsis;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
.username, .mute-thread, .mute-words {
word-wrap: normal;
word-break: normal;
white-space: nowrap;
}
.username, .mute-words {
text-overflow: ellipsis;
overflow: hidden;
}
.username {
flex: 0 1 auto;
margin-right: .2em;
}
.mute-thread {
flex: 0 0 auto;
}
.mute-words {
flex: 1 0 5em;
margin-left: .2em;
&::before {
content: ' '
}
}
.unmute {
flex: 0 0 auto;
margin-left: auto;
display: block;
margin-left: auto;
}
}
.reply-body {
flex: 1;
}
.favs-repeated-users {
margin-top: $status-margin;
.stats {
width: 100%;
display: flex;
line-height: 1em;
.stat-count {
margin-right: $status-margin;
user-select: none;
&:hover .stat-title {
text-decoration: underline;
}
.stat-title {
color: var(--faint, $fallback--faint);
font-size: 12px;
text-transform: uppercase;
position: relative;
}
.stat-number {
font-weight: bolder;
font-size: 16px;
line-height: 1em;
}
}
.avatar-row {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
height: 100%;
width: 1px;
left: 0;
background-color: var(--faint, $fallback--faint);
}
}
}
}
@media all and (max-width: 800px) {
.status-el {
.retweet-info {
.avatar.still-image {
margin-left: 20px;
}
}
}
.status {
max-width: 100%;
}
.status .avatar.still-image {
width: 40px;
height: 40px;
&.avatar-compact {
width: 32px;
height: 32px;
}
}
}
</style>
<style src="./status.scss" lang="scss"></style>

View File

@ -1,6 +1,6 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div class="status-body">
<div class="StatusContent">
<slot name="header" />
<div
v-if="status.summary_html"
@ -133,7 +133,7 @@
$status-margin: 0.75em;
.status-body {
.StatusContent {
flex: 1;
min-width: 0;
@ -275,6 +275,7 @@ $status-margin: 0.75em;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
height: 1.4em;
}
}
}
@ -283,13 +284,4 @@ $status-margin: 0.75em;
color: $fallback--cGreen;
color: var(--postGreentext, $fallback--cGreen);
}
.timeline :not(.panel-disabled) > {
.status-el:last-child {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: none;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<Popover
trigger="hover"
popover-class="status-popover"
popover-class="popover-default status-popover"
:bound-to="{ x: 'container' }"
@show="enter"
>
@ -38,7 +38,8 @@
<style lang="scss">
@import '../../_variables.scss';
.status-popover {
/* popover styles load on-demand, so we need to override */
.status-popover.popover {
font-size: 1rem;
min-width: 15em;
max-width: 95%;
@ -52,7 +53,8 @@
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
.status-el.status-el {
/* TODO cleanup this */
.Status.Status {
border: none;
}

View File

@ -30,48 +30,9 @@
position: relative;
line-height: 0;
overflow: hidden;
width: 100%;
height: 100%;
display: flex;
align-items: center;
&:hover canvas {
display: none;
}
img {
width: 100%;
min-height: 100%;
object-fit: contain;
}
&.animated {
&:hover::before,
img {
visibility: hidden;
}
&:hover img {
visibility: visible
}
&::before {
content: 'gif';
position: absolute;
line-height: 10px;
font-size: 10px;
top: 5px;
left: 5px;
background: rgba(127,127,127,.5);
color: #FFF;
display: block;
padding: 2px 4px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
z-index: 2;
}
}
canvas {
position: absolute;
top: 0;
@ -81,6 +42,45 @@
width: 100%;
height: 100%;
object-fit: contain;
visibility: var(--still-image-canvas, visible);
}
img {
width: 100%;
min-height: 100%;
object-fit: contain;
}
&.animated {
&::before {
content: 'gif';
position: absolute;
line-height: 10px;
font-size: 10px;
top: 5px;
left: 5px;
background: rgba(127, 127, 127, 0.5);
color: #fff;
display: block;
padding: 2px 4px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
z-index: 2;
visibility: var(--still-image-label-visibility, visible);
}
&:hover canvas {
display: none;
}
&:hover::before,
img {
visibility: var(--still-image-img, hidden);
}
&:hover img {
visibility: visible;
}
}
}
</style>

View File

@ -1,4 +1,5 @@
import Vue from 'vue'
import { mapState } from 'vuex'
import './tab_switcher.scss'
@ -44,7 +45,13 @@ export default Vue.component('tab-switcher', {
} else {
return this.active
}
}
},
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
...mapState({
settingsModalState: state => state.interface.settingsModalState
})
},
beforeUpdate () {
const currentSlot = this.$slots.default[this.active]
@ -134,7 +141,7 @@ export default Vue.component('tab-switcher', {
<div class="tabs">
{tabs}
</div>
<div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
<div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} v-body-scroll-lock={this.settingsModalVisible}>
{contents}
</div>
</div>

View File

@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue'
import TimelineMenu from '../timeline_menu/timeline_menu.vue'
import { throttle, keyBy } from 'lodash'
export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
@ -35,6 +36,11 @@ const Timeline = {
bottomedOut: false
}
},
components: {
Status,
Conversation,
TimelineMenu
},
computed: {
timelineError () {
return this.$store.state.statuses.error
@ -74,10 +80,6 @@ const Timeline = {
return keyBy(this.pinnedStatusIds)
}
},
components: {
Status,
Conversation
},
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials

View File

@ -1,9 +1,7 @@
<template>
<div :class="classes.root">
<div :class="[classes.root, 'timeline']">
<div :class="classes.header">
<div class="title">
{{ title }}
</div>
<TimelineMenu v-if="!embedded" />
<div
v-if="timelineError"
class="loadmore-error alert error"
@ -106,4 +104,16 @@
opacity: 1;
}
}
.timeline-heading {
max-width: 100%;
flex-wrap: nowrap;
.loadmore-button {
flex-shrink: 0;
}
.loadmore-text {
flex-shrink: 0;
line-height: 1em;
}
}
</style>

View File

@ -0,0 +1,63 @@
import Popover from '../popover/popover.vue'
import { mapState } from 'vuex'
// Route -> i18n key mapping, exported andnot in the computed
// because nav panel benefits from the same information.
export const timelineNames = () => {
return {
'friends': 'nav.timeline',
'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
'public-external-timeline': 'nav.twkn',
'tag-timeline': 'tag'
}
}
const TimelineMenu = {
components: {
Popover
},
data () {
return {
isOpen: false
}
},
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
if (timelineNames()[this.$route.name]) {
this.$store.dispatch('setLastTimeline', this.$route.name)
}
},
methods: {
openMenu () {
// $nextTick is too fast, animation won't play back but
// instead starts in fully open position. Low values
// like 1-5 work on fast machines but not on mobile, 25
// seems like a good compromise that plays without significant
// added lag.
setTimeout(() => {
this.isOpen = true
}, 25)
},
timelineName () {
const route = this.$route.name
if (route === 'tag-timeline') {
return '#' + this.$route.params.tag
}
const i18nkey = timelineNames()[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route
}
},
computed: {
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
}
}
export default TimelineMenu

View File

@ -0,0 +1,180 @@
<template>
<Popover
trigger="click"
class="timeline-menu"
:class="{ 'open': isOpen }"
:margin="{ left: -15, right: -200 }"
:bound-to="{ x: 'container' }"
popover-class="timeline-menu-popover-wrap"
@show="openMenu"
@close="() => isOpen = false"
>
<div
slot="content"
class="timeline-menu-popover panel panel-default"
>
<ul>
<li v-if="currentUser">
<router-link :to="{ name: 'friends' }">
<i class="button-icon icon-home-2" />{{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'bookmarks'}">
<i class="button-icon icon-bookmark" />{{ $t("nav.bookmarks") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
<i class="button-icon icon-mail-alt" />{{ $t("nav.dms") }}
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link :to="{ name: 'public-timeline' }">
<i class="button-icon icon-users" />{{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<i class="button-icon icon-globe" />{{ $t("nav.twkn") }}
</router-link>
</li>
</ul>
</div>
<div
slot="trigger"
class="title timeline-menu-title"
>
<span>{{ timelineName() }}</span>
<i class="icon-down-open" />
</div>
</Popover>
</template>
<script src="./timeline_menu.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.timeline-menu {
flex-shrink: 1;
margin-right: auto;
min-width: 0;
width: 24rem;
.timeline-menu-popover-wrap {
overflow: hidden;
// Match panel heading padding to line up menu with bottom of heading
margin-top: 0.6rem;
padding: 0 15px 15px 15px;
}
.timeline-menu-popover {
width: 24rem;
max-width: 100vw;
margin: 0;
font-size: 1rem;
border-top-right-radius: 0;
border-top-left-radius: 0;
transform: translateY(-100%);
transition: transform 100ms;
}
.panel::after {
border-top-right-radius: 0;
border-top-left-radius: 0;
}
&.open .timeline-menu-popover {
transform: translateY(0);
}
.timeline-menu-title {
margin: 0;
cursor: pointer;
display: flex;
user-select: none;
width: 100%;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
i {
margin-left: 0.6em;
flex-shrink: 0;
font-size: 1rem;
transition: transform 100ms;
}
}
&.open .timeline-menu-title i {
color: $fallback--text;
color: var(--panelText, $fallback--text);
transform: rotate(180deg);
}
.panel {
box-shadow: var(--popoverShadow);
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
padding: 0;
&:last-child a {
border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius;
border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
}
&:last-child {
border: none;
}
i {
margin: 0 0.5em;
}
}
a {
display: block;
padding: 0.6em 0;
&: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);
--icon: var(--selectedMenuIcon, $fallback--icon);
}
&.router-link-active {
font-weight: bolder;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--text;
color: var(--selectedMenuText, $fallback--text);
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
&:hover {
text-decoration: underline;
}
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<StillImage
class="avatar"
class="Avatar"
:alt="user.screen_name"
:title="user.screen_name"
:src="imgSrc(user.profile_image_url_original)"
@ -13,7 +13,9 @@
<style lang="scss">
@import '../../_variables.scss';
.avatar.still-image {
.Avatar {
--still-image-label-visibility: hidden;
width: 48px;
height: 48px;
box-shadow: var(--avatarStatusShadow);

View File

@ -354,7 +354,7 @@
align-items: flex-start;
max-height: 56px;
.avatar {
.Avatar {
flex: 1 0 100%;
width: 56px;
height: 56px;
@ -364,13 +364,9 @@
}
}
&:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
&:hover .Avatar {
--still-image-img: visible;
--still-image-canvas: hidden;
}
&-avatar-link {

View File

@ -20,13 +20,14 @@
:key="index"
class="user-profile-field"
>
<!-- eslint-disable vue/no-v-html -->
<dt
:title="user.fields_text[index].name"
class="user-profile-field-name"
@click.prevent="linkClicked"
v-html="field.name"
/>
>
{{ field.name }}
</dt>
<!-- eslint-disable vue/no-v-html -->
<dd
:title="user.fields_text[index].value"
class="user-profile-field-value"

View File

@ -146,7 +146,8 @@
display: flex;
justify-content: space-between;
> .status-el {
/* TODO cleanup this */
> .Status {
flex: 1;
}

View File

@ -58,7 +58,7 @@
"dms": "Direktnachrichten",
"public_tl": "Öffentliche Zeitleiste",
"timeline": "Zeitleiste",
"twkn": "Das gesamte bekannte Netzwerk",
"twkn": "Bekannte Netzwerk",
"user_search": "Benutzersuche",
"search": "Suche",
"preferences": "Voreinstellungen",

View File

@ -120,12 +120,13 @@
"dms": "Direct Messages",
"public_tl": "Public Timeline",
"timeline": "Timeline",
"twkn": "The Whole Known Network",
"twkn": "Known Network",
"bookmarks": "Bookmarks",
"user_search": "User Search",
"search": "Search",
"who_to_follow": "Who to follow",
"preferences": "Preferences",
"timelines": "Timelines",
"chats": "Chats"
},
"notifications": {

View File

@ -5,7 +5,7 @@
"features_panel": {
"chat": "Babilejo",
"gopher": "Gopher",
"media_proxy": "Aŭdvidaĵa prokurilo",
"media_proxy": "Vidaŭdaĵa prokurilo",
"scope_options": "Agordoj de amplekso",
"text_limit": "Limo de teksto",
"title": "Funkcioj",
@ -79,7 +79,8 @@
"chats": "Babiloj",
"search": "Serĉi",
"interactions": "Interagoj",
"administration": "Administrado"
"administration": "Administrado",
"bookmarks": "Legosignoj"
},
"notifications": {
"broken_favorite": "Nekonata stato, serĉante ĝin…",
@ -278,7 +279,7 @@
"true": "jes"
},
"notifications": "Sciigoj",
"enable_web_push_notifications": "Ŝalti retajn pajn sciigojn",
"enable_web_push_notifications": "Ŝalti retajn pasivajn sciigojn",
"style": {
"switcher": {
"keep_color": "Konservi kolorojn",
@ -286,10 +287,22 @@
"keep_opacity": "Konservi maltravideblecon",
"keep_roundness": "Konservi rondecon",
"keep_fonts": "Konservi tiparojn",
"save_load_hint": "Elektebloj de \"konservi\" konservas la nuntempajn agordojn dum elektado aŭ enlegado de haŭtoj. Ĝi ankaŭ konservas tiujn agordojn dum elportado de haŭto. Kun ĉiuj markbutonoj nemarkitaj, elporto de la haŭto ĉion konservos.",
"save_load_hint": "Elektebloj de «konservi» konservas la nuntempajn agordojn dum elektado aŭ enlegado de haŭtoj. Ĝi ankaŭ konservas tiujn agordojn dum elportado de haŭto. Kun ĉiuj markbutonoj nemarkitaj, elporto de la haŭto ĉion konservos.",
"reset": "Restarigi",
"clear_all": "Vakigi ĉion",
"clear_opacity": "Vakigi maltravideblecon"
"clear_opacity": "Vakigi maltravideblecon",
"help": {
"fe_downgraded": "Versio de PleromaFE reen iris.",
"fe_upgraded": "La motoro de haŭtoj de PleromaFE ĝisdatiĝis post ĝisdatigo de la versio.",
"older_version_imported": "La enportita dosiero estis farita per pli malnova versio de PleromaFE.",
"future_version_imported": "La enportita dosiero estis farita per pli nova versio de PleromaFE.",
"v2_imported": "La dosiero, kiun vi enportis, estis farita por malnova versio de PleromaFE. Ni provas maksimumigi interkonformecon, sed tamen eble montriĝos misoj.",
"upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras."
},
"use_source": "Nova versio",
"use_snapshot": "Malnova versio",
"keep_as_is": "Teni senŝanĝa",
"load_theme": "Enlegi haŭton"
},
"common": {
"color": "Koloro",
@ -297,7 +310,7 @@
"contrast": {
"hint": "Proporcio de kontrasto estas {ratio}, ĝi {level} {context}",
"level": {
"aa": "plenumas la gvidilon je nivelo AA (malpleja)",
"aa": "plenumas la gvidilon je nivelo AA (minimuma)",
"aaa": "plenumas la gvidilon je nivela AAA (rekomendita)",
"bad": "plenumas neniujn faciluzajn gvidilojn"
},
@ -310,21 +323,39 @@
"common_colors": {
"_tab_label": "Komunaj",
"main": "Komunaj koloroj",
"foreground_hint": "Vidu langeton \"Specialaj\" por pli detalaj agordoj",
"foreground_hint": "Vidu langeton «Specialaj» por pli detalaj agordoj",
"rgbo": "Bildsimboloj, emfazoj, insignoj"
},
"advanced_colors": {
"_tab_label": "Specialaj",
"alert": "Averta fono",
"alert": "Fono de averto",
"alert_error": "Eraro",
"badge": "Insigna fono",
"badge": "Fono de insigno",
"badge_notification": "Sciigo",
"panel_header": "Kapo de breto",
"top_bar": "Supra breto",
"borders": "Limoj",
"buttons": "Butonoj",
"inputs": "Enigaj kampoj",
"faint_text": "Malvigla teksto"
"faint_text": "Malvigla teksto",
"chat": {
"border": "Limo",
"outgoing": "Eliraj",
"incoming": "Envenaj"
},
"tabs": "Langetoj",
"disabled": "Malŝaltita",
"selectedMenu": "Elektita menuero",
"selectedPost": "Elektita afiŝo",
"pressed": "Premita",
"highlight": "Emfazitaj eroj",
"icons": "Bildsimboloj",
"poll": "Grafo de enketo",
"underlay": "Subtavolo",
"popover": "Ŝpruchelpiloj, menuoj",
"post": "Afiŝoj/Priskriboj de uzantoj",
"alert_neutral": "Neŭtrala",
"alert_warning": "Averto"
},
"radii": {
"_tab_label": "Rondeco"
@ -339,7 +370,7 @@
"inset": "Internigo",
"hint": "Por ombroj vi ankaŭ povas uzi --variable kiel koloran valoron, por uzi variantojn de CSS3. Bonvolu rimarki, ke tiuokaze agordoj de maltravidebleco ne funkcios.",
"filter_hint": {
"always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo ĝin subtenas.",
"always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo tion subtenas.",
"drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ŝlosilvorton {2}.",
"avatar_inset": "Bonvolu rimarki, ke agordi ambaŭ internajn kaj eksterajn ombrojn por profilbildoj povas redoni neatenditajn rezultojn ĉe profilbildoj travideblaj.",
"spread_zero": "Ombroj kun vastigo > 0 aperos kvazaŭ ĝi estus fakte nulo",
@ -355,13 +386,13 @@
"button": "Butono",
"buttonHover": "Butono (je ŝvebo)",
"buttonPressed": "Butono (premita)",
"buttonPressedHover": "Butono (premita je ŝvebo)",
"buttonPressedHover": "Butono (premita kaj je ŝvebo)",
"input": "Eniga kampo"
}
},
"fonts": {
"_tab_label": "Tiparoj",
"help": "Elektu tiparon uzotan por eroj de la fasado. Por \"propra\" vi devas enigi la precizan nomon de tiparo tiel, kiel ĝi aperas en la sistemo.",
"help": "Elektu tiparon uzotan por eroj de la fasado. Por «propra» vi devas enigi la precizan nomon de tiparo tiel, kiel ĝi aperas en la sistemo.",
"components": {
"interface": "Fasado",
"input": "Enigaj kampoj",
@ -416,7 +447,8 @@
"recovery_codes": "Rehavaj kodoj.",
"warning_of_generate_new_codes": "Kiam vi estigos novajn rehavajn kodojn, viaj malnovaj ne plu funkcios.",
"generate_new_recovery_codes": "Estigi novajn rehavajn kodojn",
"title": "Duobla aŭtentikigo"
"title": "Duobla aŭtentikigo",
"otp": "OTP"
},
"enter_current_password_to_confirm": "Enigu vian pasvorton por konfirmi vian identecon",
"security": "Sekureco",
@ -450,64 +482,109 @@
"hide_muted_posts": "Kaŝi afiŝojn de silentigitaj uzantoj",
"emoji_reactions_on_timeline": "Montri bildosignajn reagojn en la tempolinio",
"pad_emoji": "Meti spacetojn ĉirkaŭ bildosigno post ties elekto",
"domain_mutes": "Retnomoj"
"domain_mutes": "Retnomoj",
"notification_blocks": "Blokinte uzanton vi malabonos ĝin kaj haltigos ĉiujn sciigojn.",
"notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu.",
"notification_setting_hide_notification_contents": "Kaŝi la sendinton kaj la enhavojn de pasivaj sciigoj",
"notification_setting_privacy": "Privateco",
"notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas",
"notification_setting_filters": "Filtriloj",
"greentext": "Memecitaĵoj",
"version": {
"frontend_version": "Versio de fasado",
"backend_version": "Versio de internaĵo",
"title": "Versio"
},
"accent": "Emfazo"
},
"timeline": {
"collapse": "Maletendi",
"conversation": "Interparolo",
"error_fetching": "Eraro dum ĝisdatigo",
"error_fetching": "Eraris ĝisdatigo",
"load_older": "Montri pli malnovajn statojn",
"no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti",
"repeated": "ripetita",
"show_new": "Montri novajn",
"up_to_date": "Ĝisdata",
"no_more_statuses": "Neniuj pliaj statoj",
"no_statuses": "Neniuj statoj"
"no_statuses": "Neniuj statoj",
"reload": "Enlegi ree"
},
"user_card": {
"approve": "Aprobi",
"block": "Bari",
"blocked": "Barita!",
"block": "Bloki",
"blocked": "Blokita!",
"deny": "Rifuzi",
"favorites": "Ŝatataj",
"follow": "Aboni",
"follow_sent": "Peto sendiĝis!",
"follow_progress": "Petanta…",
"follow_again": "Ĉu sendi peton denove?",
"follow_progress": "Petante…",
"follow_again": "Ĉu sendi peton ree?",
"follow_unfollow": "Malaboni",
"followees": "Abonatoj",
"followers": "Abonantoj",
"following": "Abonanta!",
"following": "Abonata!",
"follows_you": "Abonas vin!",
"its_you": "Tio estas vi!",
"media": "Aŭdvidaĵoj",
"media": "Vidaŭdaĵoj",
"mute": "Silentigi",
"muted": "Silentigitaj",
"muted": "Silentigita",
"per_day": "tage",
"remote_follow": "Fore aboni",
"statuses": "Statoj",
"unblock": "Malbari",
"unblock_progress": "Malbaranta…",
"block_progress": "Baranta…",
"unblock": "Malbloki",
"unblock_progress": "Malblokante…",
"block_progress": "Blokante…",
"unmute": "Malsilentigi",
"unmute_progress": "Malsilentiganta…",
"mute_progress": "Silentiganta…"
"unmute_progress": "Malsilentigante…",
"mute_progress": "Silentigante…",
"report": "Raporti",
"message": "Mesaĝo",
"mention": "Mencio",
"hidden": "Kaŝita",
"admin_menu": {
"delete_user_confirmation": "Ĉu vi tute certas? Ĉi tiu ago ne estas malfarebla.",
"delete_user": "Forigi uzanton",
"quarantine": "Malpermesi federadon de afiŝoj de uzanto",
"disable_any_subscription": "Malpermesi ĉian abonadon al uzanto",
"disable_remote_subscription": "Malpermesi abonadon al uzanto el foraj nodoj",
"sandbox": "Devigi afiŝojn esti nur por abonantoj",
"force_unlisted": "Devigi afiŝojn nelistiĝi",
"strip_media": "Forigi vidaŭdaĵojn de afiŝoj",
"force_nsfw": "Marki ĉiujn afiŝojn konsternaj",
"delete_account": "Forigi konton",
"deactivate_account": "Malaktivigi konton",
"activate_account": "Aktivigi konton",
"revoke_moderator": "Malnomumi reguligiston",
"grant_moderator": "Nomumi reguligiston",
"revoke_admin": "Malnomumi administranton",
"grant_admin": "Nomumi administranton",
"moderation": "Reguligado"
},
"show_repeats": "Montri ripetojn",
"hide_repeats": "Kaŝi ripetojn",
"unsubscribe": "Ne ricevi sciigojn",
"subscribe": "Ricevi sciigojn"
},
"user_profile": {
"timeline_title": "Uzanta tempolinio",
"timeline_title": "Historio de uzanto",
"profile_does_not_exist": "Pardonu, ĉi tiu profilo ne ekzistas.",
"profile_loading_error": "Pardonu, eraro okazis dum enlegado de ĉi tiu profilo."
"profile_loading_error": "Pardonu, eraris enlego de ĉi tiu profilo."
},
"who_to_follow": {
"more": "Pli",
"who_to_follow": "Kiun aboni"
},
"tool_tip": {
"media_upload": "Alŝuti aŭdvidaĵon",
"media_upload": "Alŝuti vidaŭdaĵon",
"repeat": "Ripeti",
"reply": "Respondi",
"favorite": "Ŝati",
"user_settings": "Agordoj de uzanto"
"user_settings": "Agordoj de uzanto",
"bookmark": "Legosigno",
"reject_follow_request": "Rifuzi abonpeton",
"accept_follow_request": "Akcepti abonpeton",
"add_reaction": "Aldoni reagon"
},
"upload": {
"error": {
@ -580,7 +657,7 @@
"accept": "Akcepti",
"simple_policies": "Specialaj politikoj de la nodo"
},
"mrf_policies": "Ŝaltis politikon de MRF",
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
"keyword": {
"is_replaced_by": "→",
"replace": "Anstataŭigi",
@ -588,7 +665,8 @@
"ftl_removal": "Forigo de la historio de «La tuta konata reto»",
"keyword_policies": "Politiko pri ŝlosilvortoj"
},
"federation": "Federado"
"federation": "Federado",
"mrf_policies_desc": "Politikoj de Mesaĝa ŝanĝilaro (MRF) efikas sur federa konduto de la nodo. La sekvaj politikoj estas ŝaltitaj:"
}
},
"selectable_list": {
@ -607,5 +685,112 @@
},
"errors": {
"storage_unavailable": "Pleroma ne povis aliri deponejon de la foliumilo. Via saluto kaj viaj lokaj agordoj ne estos konservitaj, kaj vi eble renkontos neatenditajn problemojn. Provu permesi kuketojn."
},
"status": {
"hide_content": "Kaŝi enhavon",
"show_content": "Montri enhavon",
"hide_full_subject": "Kaŝi plenan temon",
"show_full_subject": "Montri plenan temon",
"thread_muted_and_words": ", enhavas vortojn:",
"thread_muted": "Fadeno silentigita",
"copy_link": "Kopii ligilon al stato",
"status_unavailable": "Stato ne estas disponebla",
"unmute_conversation": "Malsilentigi interparolon",
"mute_conversation": "Silentigi interparolon",
"replies_list": "Respondoj:",
"reply_to": "Responde al",
"delete_confirm": "Ĉu vi certe volas forigi ĉi tiun staton?",
"unbookmark": "Senlegosigni",
"bookmark": "Legosigni",
"pinned": "Fiksita",
"unpin": "Malfiksi de profilo",
"pin": "Fiksi al profilo",
"delete": "Forigi staton",
"repeats": "Ripetoj",
"favorites": "Ŝatataj"
},
"time": {
"years_short": "{0}j",
"year_short": "{0}j",
"years": "{0} jaroj",
"year": "{0} jaro",
"weeks_short": "{0}s",
"week_short": "{0}s",
"weeks": "{0} semajnoj",
"week": "{0} semajno",
"seconds_short": "{0}s",
"second_short": "{0}s",
"seconds": "{0} sekundoj",
"second": "{0} sekundo",
"now_short": "nun",
"now": "ĵus",
"months_short": "{0}m",
"month_short": "{0}m",
"months": "{0} monatoj",
"month": "{0} monato",
"minutes_short": "{0}m",
"minute_short": "{0}m",
"minutes": "{0} minutoj",
"minute": "{0} minuto",
"in_past": "antaŭ {0}",
"in_future": "post {0}",
"hours_short": "{0}h",
"hour_short": "{0}h",
"hours": "{0} horoj",
"hour": "{0} horo",
"days_short": "{0}t",
"day_short": "{0}t",
"days": "{0} tagoj",
"day": "{0} tago"
},
"search": {
"people": "Personoj",
"no_results": "Neniuj rezultoj",
"people_talking": "{count} personoj parolas",
"person_talking": "{count} persono parolas",
"hashtags": "Kradvortoj"
},
"display_date": {
"today": "Hodiaŭ"
},
"file_type": {
"file": "Dosiero",
"image": "Bildo",
"video": "Filmo",
"audio": "Sono"
},
"chats": {
"empty_chat_list_placeholder": "Vi ankoraŭ havas neniun babilon. Komencu novan babilon!",
"error_sending_message": "Io misokazis dum sendado de la mesaĝo.",
"error_loading_chat": "Io misokazis dum enlego de la babilo.",
"delete_confirm": "Ĉu vi certe volas forigi ĉi tiun mesaĝon?",
"more": "Pli",
"empty_message_error": "Ne povas sendi malplenan mesaĝon",
"new": "Nova babilo",
"chats": "Babiloj",
"delete": "Forigi",
"you": "Vi:"
},
"password_reset": {
"password_reset_required_but_mailer_is_disabled": "Vi devas restarigi vian pasvorton, sed restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.",
"password_reset_required": "Vi devas restarigi vian pasvorton por saluti.",
"password_reset_disabled": "Restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.",
"too_many_requests": "Vi atingis la limon de provoj, reprovu pli poste.",
"not_found": "Ni ne trovis tiun retpoŝtadreson aŭ uzantonomon.",
"return_home": "Reiri al la hejmpaĝo",
"check_email": "Kontrolu vian retpoŝton pro ligilo por restarigi vian pasvorton.",
"placeholder": "Via retpoŝtadreso aŭ uzantonomo",
"instruction": "Enigu vian retpoŝtadreson aŭ uzantonomon. Ni sendos al vi ligilon por restarigi vian pasvorton.",
"password_reset": "Restarigi pasvorton",
"forgot_password": "Ĉu vi forgesis pasvorton?"
},
"user_reporting": {
"generic_error": "Eraris traktado de via peto.",
"submit": "Sendi",
"forward_to": "Plusendi al {0}",
"forward_description": "La konto venas de alia servilo. Ĉu kopio de la raporto sendiĝu ankaŭ tien?",
"additional_comments": "Aldonaj komentoj",
"add_comment_description": "Ĉi tiu raporto sendiĝos al reguligistoj de via nodo. Vi povas komprenigi kial vi raportas ĉi tiun konton sube:",
"title": "Raportante {0}"
}
}

View File

@ -63,7 +63,7 @@
"dms": "Yksityisviestit",
"public_tl": "Julkinen Aikajana",
"timeline": "Aikajana",
"twkn": "Koko Tunnettu Verkosto",
"twkn": "Tunnettu Verkosto",
"user_search": "Käyttäjähaku",
"who_to_follow": "Seurausehdotukset",
"preferences": "Asetukset",

View File

@ -36,7 +36,8 @@
"who_to_follow": "Chi seguire",
"preferences": "Preferenze",
"bookmarks": "Segnalibri",
"chats": "Conversazioni"
"chats": "Conversazioni",
"timelines": "Sequenze"
},
"notifications": {
"followed_you": "ti segue",
@ -805,5 +806,8 @@
"delete": "Elimina",
"message_user": "Contatta {nickname}",
"you": "Tu:"
},
"shoutbox": {
"title": "Graffiti"
}
}

View File

@ -196,7 +196,8 @@
"unlisted": "Niewidoczny Nie umieszczaj na publicznych osiach czasu"
},
"preview_empty": "Pusty",
"preview": "Podgląd"
"preview": "Podgląd",
"empty_status_error": "Nie można wysłać pustego wpisu bez plików"
},
"registration": {
"bio": "Bio",
@ -345,8 +346,8 @@
"notification_visibility_moves": "Użytkownik migruje",
"notification_visibility_emoji_reactions": "Reakcje",
"no_rich_text_description": "Usuwaj formatowanie ze wszystkich postów",
"no_blocks": "Bez blokad",
"no_mutes": "Bez wyciszeń",
"no_blocks": "Brak blokad",
"no_mutes": "Brak wyciszeń",
"hide_follows_description": "Nie pokazuj kogo obserwuję",
"hide_followers_description": "Nie pokazuj kto mnie obserwuje",
"hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
@ -488,7 +489,11 @@
"selectedMenu": "Wybrany element menu",
"disabled": "Wyłączone",
"toggled": "Przełączone",
"tabs": "Karty"
"tabs": "Karty",
"chat": {
"outgoing": "Wiadomości wychodzące",
"incoming": "Wiadomości przychodzące"
}
},
"radii": {
"_tab_label": "Zaokrąglenie"
@ -563,9 +568,12 @@
"reset_avatar": "Zresetuj awatar",
"profile_fields": {
"value": "Zawartość",
"label": "Metadane profilu"
"label": "Metadane profilu",
"name": "Nazwa",
"add_field": "Dodaj pole"
},
"bot": "To konto jest prowadzone przez bota"
"bot": "To konto jest prowadzone przez bota",
"notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push"
},
"time": {
"day": "{0} dzień",
@ -627,7 +635,11 @@
"mute_conversation": "Wycisz konwersację",
"unmute_conversation": "Odcisz konwersację",
"status_unavailable": "Status niedostępny",
"copy_link": "Kopiuj link do statusu"
"copy_link": "Kopiuj link do statusu",
"unbookmark": "Usuń z zakładek",
"bookmark": "Dodaj do zakładek",
"hide_content": "Ukryj zawartość",
"show_content": "Pokaż zawartość"
},
"user_card": {
"approve": "Przyjmij",
@ -682,7 +694,8 @@
"quarantine": "Zakaż federowania postów od tego użytkownika",
"delete_user": "Usuń użytkownika",
"delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta."
}
},
"message": "Napisz"
},
"user_profile": {
"timeline_title": "Oś czasu użytkownika",
@ -747,12 +760,21 @@
"password_reset_required_but_mailer_is_disabled": "Musisz zresetować hasło, ale resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji."
},
"file_type": {
"file": "Plik"
"file": "Plik",
"image": "Zdjęcie",
"video": "Wideo",
"audio": "Audio"
},
"chats": {
"more": "Więcej",
"delete": "Usuń",
"you": "Ty:"
"you": "Ty:",
"delete_confirm": "Czy na pewno chcesz usunąć tą wiadomość?",
"message_user": "Napisz do {nickname}",
"error_sending_message": "Coś poszło nie tak podczas wysyłania wiadomości.",
"error_loading_chat": "Coś poszło nie tak podczas ładowania czatu.",
"empty_message_error": "Nie można wysłać pustej wiadomości",
"new": "Nowy czat"
},
"display_date": {
"today": "Dzisiaj"

View File

@ -16,7 +16,8 @@ const defaultState = {
},
mobileLayout: false,
globalNotices: [],
layoutHeight: 0
layoutHeight: 0,
lastTimeline: null
}
const interfaceMod = {
@ -69,6 +70,9 @@ const interfaceMod = {
},
setLayoutHeight (state, value) {
state.layoutHeight = value
},
setLastTimeline (state, value) {
state.lastTimeline = value
}
},
actions: {
@ -117,6 +121,9 @@ const interfaceMod = {
},
setLayoutHeight ({ commit }, value) {
commit('setLayoutHeight', value)
},
setLastTimeline ({ commit }, value) {
commit('setLastTimeline', value)
}
}
}

View File

@ -505,6 +505,7 @@ const users = {
store.commit('clearNotifications')
store.commit('resetStatuses')
store.dispatch('resetChats')
store.dispatch('setLastTimeline', 'public-timeline')
})
},
loginUser (store, accessToken) {

View File

@ -209,6 +209,7 @@ export const parseAttachment = (data) => {
}
output.url = data.url
output.large_thumb_url = data.preview_url
output.description = data.description
return output

View File

@ -330,7 +330,7 @@ describe('Statuses module', () => {
const deletion = makeMockStatus({ id: '4', type: 'deletion' })
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
deletion.uri = 'xxx'
const newNotificationSideEffects = () => {}
mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
mutations.addNewNotifications(
state,
@ -342,7 +342,8 @@ describe('Statuses module', () => {
status: otherStatus,
action: otherStatus,
seen: false
}]
}],
newNotificationSideEffects
})
expect(state.notifications.data.length).to.eql(1)
@ -356,7 +357,8 @@ describe('Statuses module', () => {
status: mentionedStatus,
action: mentionedStatus,
seen: false
}]
}],
newNotificationSideEffects
})
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })

View File

@ -17,8 +17,7 @@ const message3 = {
created_at: (new Date('2020-07-22T18:45:59.000Z'))
}
// TODO: only
describe.only('chatService', () => {
describe('chatService', () => {
describe('.add', () => {
it("Doesn't add duplicates", () => {
const chat = chatService.empty()

1437
yarn.lock

File diff suppressed because it is too large Load Diff