Merge branch 'threecolumn' into shigusegubu-vue3

* threecolumn:
  Fix active popover style
  Use panel text instead of text for shoutbox icon
  Fix dropdown menu style inside panel header
  Fix phoenix sockets in dev mode
  Fix no reactivity on vuex 4 values
  fix tegulu
  heck
  teleport bread
  wide mode initial implementation + cleanup
  make chatlist header sticky
  fix random error that sometimes occurs
  cleanup & code splitting
  fix chat loading endlessly
  chats work and look a bit better
  fix main column having wild widths
  fixed tons of stuff, at least it looks normalish on desktop
  refactored how main app layout works
This commit is contained in:
Henry Jameson 2022-04-07 10:34:17 +03:00
commit 4e74e9d08c
31 changed files with 704 additions and 613 deletions

View file

@ -52,7 +52,10 @@ module.exports = {
target, target,
changeOrigin: true, changeOrigin: true,
cookieDomainRewrite: 'localhost', cookieDomainRewrite: 'localhost',
ws: true ws: true,
headers: {
'Origin': target
}
}, },
'/oauth/revoke': { '/oauth/revoke': {
target, target,

View file

@ -1,6 +1,5 @@
import UserPanel from './components/user_panel/user_panel.vue' import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue' import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
@ -16,13 +15,14 @@ import PostStatusModal from './components/post_status_modal/post_status_modal.vu
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { defineAsyncComponent } from 'vue'
export default { export default {
name: 'app', name: 'app',
components: { components: {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications, Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -65,7 +65,7 @@ export default {
} }
} }
}, },
shout () { return this.$store.state.shout.channel.state === 'joined' }, shout () { return this.$store.state.shout.joined },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () { showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel && return this.$store.state.instance.showInstanceSpecificPanel &&
@ -79,22 +79,20 @@ export default {
hideShoutbox () { hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox return this.$store.getters.mergedConfig.hideShoutbox
}, },
isMobileLayout () { return this.$store.state.interface.mobileLayout }, layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private }, privateMode () { return this.$store.state.instance.private },
sidebarAlign () { reverseLayout () { return this.$store.getters.mergedConfig.sidebarRight },
return {
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
}
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
methods: { methods: {
updateMobileState () { updateMobileState () {
const mobileLayout = windowWidth() <= 800 const mobileLayout = windowWidth() <= 800
const wideLayout = windowWidth() >= 1300
const layoutHeight = windowHeight() const layoutHeight = windowHeight()
const changed = mobileLayout !== this.isMobileLayout const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
const changed = layoutType !== this.layoutType
if (changed) { if (changed) {
this.$store.dispatch('setMobileLayout', mobileLayout) this.$store.dispatch('setLayoutType', layoutType)
} }
this.$store.dispatch('setLayoutHeight', layoutHeight) this.$store.dispatch('setLayoutHeight', layoutHeight)
} }

View file

@ -1,62 +1,21 @@
// stylelint-disable rscss/class-format
@import './_variables.scss'; @import './_variables.scss';
#app {
min-height: 100vh;
max-width: 100%;
overflow: hidden;
}
.app-bg-wrapper {
position: fixed;
z-index: -1;
height: 100%;
left: 0;
right: -20px;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-position: 50% 50px;
}
i[class^='icon-'] {
user-select: none;
}
h4 {
margin: 0;
}
#content {
box-sizing: border-box;
padding-top: 60px;
margin: auto;
min-height: 100vh;
max-width: 980px;
align-content: flex-start;
}
.underlay {
background-color: rgba(0,0,0,0.15);
background-color: var(--underlay, rgba(0,0,0,0.15));
}
.text-center {
text-align: center;
}
html { html {
font-size: 14px; font-size: 14px;
overflow: hidden;
max-height: 100vh;
} }
body { body {
overscroll-behavior-y: none; overflow: hidden;
max-height: 100vh;
max-width: 100vw;
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
margin: 0; margin: 0;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
max-width: 100vw;
overflow-x: hidden; overflow-x: hidden;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -72,6 +31,171 @@ a {
color: var(--link, $fallback--link); color: var(--link, $fallback--link);
} }
h4 {
margin: 0;
}
nav {
z-index: 1000;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
color: $fallback--faint;
color: var(--faint, $fallback--faint);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--topBarShadow);
box-sizing: border-box;
height: var(--navbar-height);
}
#app-loaded {
min-height: 100vh;
min-width: 100vw;
overflow: hidden;
--navbar-height: 50px;
}
#content {
overscroll-behavior-y: none;
overflow-y: auto;
position: sticky;
}
#sidebar {
grid-area: sidebar;
}
#notifs-column {
grid-area: notifs;
}
#main-scroller {
grid-area: content;
}
.app-bg-wrapper {
position: fixed;
height: 100%;
top: var(--navbar-height);
z-index: -1000;
left: 0;
right: -20px;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-position: 50%;
}
.underlay {
grid-column-start: 1;
grid-column-end: span 3;
grid-row-start: 1;
grid-row-end: 1;
margin: 0 -0.5em;
padding: 0 0.5em;
pointer-events: none;
background-color: rgba(0, 0, 0, 0.15);
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
z-index: -2000;
}
.app-layout {
position: relative;
display: grid;
grid-template-columns: var(--miniColumn) var(--maxiColumn);
grid-template-areas: "sidebar content";
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
height: calc(100vh - var(--navbar-height));
align-content: flex-start;
flex-wrap: wrap;
padding: 0 10px 0 10px;
justify-content: center;
--miniColumn: 345px;
--maxiColumn: minmax(345px, 615px);
.column {
display: grid;
grid-template-columns: 100%;
box-sizing: border-box;
padding-top: 10px;
grid-row-start: 1;
grid-row-end: 1;
margin: 0 0.5em;
row-gap: 1em;
align-content: start;
&.-scrollable {
padding-top: 10px;
position: sticky;
top: 0;
max-height: calc(100vh - var(--navbar-height));
overflow-y: auto;
overflow-x: hidden;
.panel-heading.-sticky {
top: -10px;
}
}
}
.column-inner {
display: grid;
grid-template-columns: 100%;
box-sizing: border-box;
row-gap: 1em;
align-content: start;
}
&.-reverse:not(.-wide):not(.-mobile) {
grid-template-columns: var(--maxiColumn) var(--miniColumn);
grid-template-areas: "content sidebar";
}
&.-wide {
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
grid-template-areas: "sidebar content notifs";
&.-reverse {
grid-template-areas: "notifs content sidebar";
}
}
&.-mobile {
grid-template-columns: 100vw;
grid-template-areas: "content";
padding: 0;
.column {
margin: 0;
}
.underlay {
display: none;
}
#sidebar {
display: none;
}
}
}
#content,
.column.-scrollable {
&::-webkit-scrollbar {
display: block;
width: 0;
}
}
.text-center {
text-align: center;
}
.button-default { .button-default {
user-select: none; user-select: none;
color: $fallback--text; color: $fallback--text;
@ -103,12 +227,12 @@ a {
} }
&:hover { &:hover {
box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3); box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
box-shadow: var(--buttonHoverShadow); box-shadow: var(--buttonHoverShadow);
} }
&:active { &:active {
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
@ -141,7 +265,7 @@ a {
color: var(--btnToggledText, $fallback--text); color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnToggled, $fallback--fg); background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
svg, svg,
@ -191,8 +315,9 @@ a {
} }
} }
input, textarea, .input { input,
textarea,
.input {
&.unstyled { &.unstyled {
border-radius: 0; border-radius: 0;
background: none; background: none;
@ -203,7 +328,7 @@ input, textarea, .input {
border: none; border: none;
border-radius: $fallback--inputRadius; border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset; box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow); box-shadow: var(--inputShadow);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
@ -219,9 +344,11 @@ input, textarea, .input {
height: 28px; height: 28px;
line-height: 16px; line-height: 16px;
hyphens: none; hyphens: none;
padding: 8px .5em; padding: 8px 0.5em;
&:disabled, &[disabled=disabled], &.disabled { &:disabled,
&[disabled=disabled],
&.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
@ -236,18 +363,21 @@ input, textarea, .input {
&[type=radio] { &[type=radio] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset; box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset; box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
background-color: var(--accent, $fallback--link); background-color: var(--accent, $fallback--link);
} }
&:disabled { &:disabled {
&, &,
& + label, & + label,
& + label::before { & + label::before {
opacity: .5; opacity: 0.5;
} }
} }
+ label::before { + label::before {
flex-shrink: 0; flex-shrink: 0;
display: inline-block; display: inline-block;
@ -256,9 +386,9 @@ input, textarea, .input {
width: 1.1em; width: 1.1em;
height: 1.1em; height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0px 0px 2px black inset; box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow); box-shadow: var(--inputShadow);
margin-right: .5em; margin-right: 0.5em;
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
vertical-align: top; vertical-align: top;
@ -274,17 +404,20 @@ input, textarea, .input {
&[type=checkbox] { &[type=checkbox] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
} }
&:disabled { &:disabled {
&, &,
& + label, & + label,
& + label::before { & + label::before {
opacity: .5; opacity: 0.5;
} }
} }
+ label::before { + label::before {
flex-shrink: 0; flex-shrink: 0;
display: inline-block; display: inline-block;
@ -294,9 +427,9 @@ input, textarea, .input {
height: 1.1em; height: 1.1em;
border-radius: $fallback--checkboxRadius; border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius); border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0px 0px 2px black inset; box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow); box-shadow: var(--inputShadow);
margin-right: .5em; margin-right: 0.5em;
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
vertical-align: top; vertical-align: top;
@ -324,6 +457,7 @@ option {
.hide-number-spinner { .hide-number-spinner {
-moz-appearance: textfield; -moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button, &[type=number]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button { &[type=number]::-webkit-outer-spin-button {
opacity: 0; opacity: 0;
@ -331,7 +465,8 @@ option {
} }
} }
i[class*=icon-], .svg-inline--fa { i[class*=icon-],
.svg-inline--fa {
color: $fallback--icon; color: $fallback--icon;
color: var(--icon, $fallback--icon); color: var(--icon, $fallback--icon);
} }
@ -362,273 +497,16 @@ i[class*=icon-], .svg-inline--fa {
} }
} }
.container { @import './panel.scss';
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0 10px 0 10px;
}
.auto-size {
flex: 1
}
main-router {
flex: 1;
}
.status.compact {
color: rgba(0, 0, 0, 0.42);
font-weight: 300;
p {
margin: 0;
font-size: 0.8em
}
}
/* Panel */
.panel {
display: flex;
position: relative;
flex-direction: column;
margin: 0.5em;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::after, & {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
}
}
.panel-body:empty::before {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
margin: 1em;
text-align: center;
}
.panel-heading {
display: flex;
flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: .6em .6em;
text-align: left;
line-height: 28px;
color: var(--panelText);
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
align-items: baseline;
box-shadow: var(--panelHeaderShadow);
.title {
flex: 1 0 auto;
font-size: 1.3em;
}
.faint {
background-color: transparent;
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
.button-default,
.alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: .5em;
min-width: 1px;
align-self: stretch;
}
.button-default {
flex-shrink: 0;
&,
i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedPanel, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedPanelText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledPanelText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledPanelText, $fallback--text);
}
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
}
}
.panel-heading.stub {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
/* TODO Should remove timeline-footer from here when we refactor panels into
* separate component and utilize slots
*/
.panel-footer, .timeline-footer {
display: flex;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
flex: none;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
align-items: baseline;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link);
}
}
.panel-body > p {
line-height: 18px;
padding: 1em;
margin: 0;
}
.container > * {
min-width: 0px;
}
.fa { .fa {
color: grey; color: grey;
} }
nav {
z-index: 1000;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
color: $fallback--faint;
color: var(--faint, $fallback--faint);
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow);
box-sizing: border-box;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s
}
.fade-enter-from, .fade-leave-active {
opacity: 0
}
.main {
flex-basis: 50%;
flex-grow: 1;
flex-shrink: 1;
}
.sidebar-bounds {
flex: 0;
flex-basis: 35%;
}
.sidebar-flexer {
flex: 1;
flex-basis: 345px;
width: 365px;
}
.mobile-shown { .mobile-shown {
display: none; display: none;
} }
@media all and (min-width: 800px) {
body {
overflow-y: scroll;
}
.sidebar-bounds {
overflow: hidden;
max-height: 100vh;
width: 345px;
position: fixed;
margin-top: -10px;
.sidebar-scroller {
height: 96vh;
width: 365px;
padding-top: 10px;
padding-right: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.sidebar {
width: 345px;
}
}
.sidebar-flexer {
max-height: 96vh;
flex-shrink: 0;
flex-grow: 0;
}
}
.badge { .badge {
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
@ -712,7 +590,7 @@ nav {
} }
.visibility-notice { .visibility-notice {
padding: .5em; padding: 0.5em;
border: 1px solid $fallback--faint; border: 1px solid $fallback--faint;
border: 1px solid var(--faint, $fallback--faint); border: 1px solid var(--faint, $fallback--faint);
border-radius: $fallback--inputRadius; border-radius: $fallback--inputRadius;
@ -727,7 +605,7 @@ nav {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
padding: .5em; padding: 0.5em;
color: inherit; color: inherit;
} }
} }
@ -744,72 +622,6 @@ nav {
} }
} }
@keyframes shakeError {
0% {
transform: translateX(0);
}
15% {
transform: translateX(0.375rem);
}
30% {
transform: translateX(-0.375rem);
}
45% {
transform: translateX(0.375rem);
}
60% {
transform: translateX(-0.375rem);
}
75% {
transform: translateX(0.375rem);
}
90% {
transform: translateX(-0.375rem);
}
100% {
transform: translateX(0);
}
}
@media all and (max-width: 800px) {
.mobile-hidden {
display: none;
}
.panel-switcher {
display: flex;
}
.container {
padding: 0;
}
.panel {
margin: 0.5em 0 0.5em 0;
}
.menu-button {
display: block;
margin-right: 0.8em;
}
.main {
margin-bottom: 7em;
}
}
.setting-list,
.option-list{
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em
}
}
.login-hint { .login-hint {
text-align: center; text-align: center;
@ -819,7 +631,7 @@ nav {
a { a {
display: inline-block; display: inline-block;
padding: 1em 0px; padding: 1em 0;
width: 100%; width: 100%;
} }
} }
@ -828,9 +640,46 @@ nav {
min-height: 28px; min-height: 28px;
} }
.animate-spin { .new-status-notification {
animation: spin 2s infinite linear; position: relative;
display: inline-block; font-size: 1.1em;
z-index: 1;
flex: 1;
}
@media all and (min-width: 800px) {
.sidebar-bounds {
overflow: hidden;
max-height: 100vh;
width: 345px;
position: fixed;
margin-top: -10px;
.sidebar-scroller {
height: 96vh;
width: 365px;
padding-top: 10px;
padding-right: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.sidebar {
width: 345px;
}
}
.sidebar-flexer {
max-height: 96vh;
flex-shrink: 0;
flex-grow: 0;
}
}
@media all and (max-width: 800px) {
.mobile-hidden {
display: none;
}
} }
@keyframes spin { @keyframes spin {
@ -843,49 +692,47 @@ nav {
} }
} }
.new-status-notification { @keyframes shakeError {
position: relative; 0% {
font-size: 1.1em; transform: translateX(0);
z-index: 1;
flex: 1;
}
.chat-layout {
// Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
overflow: hidden;
height: 100%;
// Get rid of scrollbar on body as scrolling happens on different element
body {
overflow: hidden;
} }
// Ensures the fixed position of the mobile browser bars on scroll up / down events. 15% {
// Prevents the mobile browser bars from overlapping or hiding the message posting form. transform: translateX(0.375rem);
@media all and (max-width: 800px) { }
body {
height: 100%;
}
#app { 30% {
height: 100%; transform: translateX(-0.375rem);
overflow: hidden; }
min-height: auto;
}
#app_bg_wrapper { 45% {
overflow: hidden; transform: translateX(0.375rem);
} }
.main { 60% {
overflow: hidden; transform: translateX(-0.375rem);
height: 100%; }
}
#content { 75% {
padding-top: 0; transform: translateX(0.375rem);
height: 100%; }
overflow: visible;
} 90% {
transform: translateX(-0.375rem);
}
100% {
transform: translateX(0);
} }
} }
// Vue transitions
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}

View file

@ -7,33 +7,26 @@
id="app_bg_wrapper" id="app_bg_wrapper"
class="app-bg-wrapper" class="app-bg-wrapper"
/> />
<MobileNav v-if="isMobileLayout" /> <MobileNav v-if="layoutType === 'mobile'" />
<DesktopNav v-else /> <DesktopNav v-else />
<div class="app-bg-wrapper app-container-wrapper" /> <notifications v-if="currentUser" />
<div <div
id="content" id="content"
class="container underlay" class="app-layout container"
:class="[{ '-reverse': reverseLayout }, '-' + layoutType]"
> >
<div <div class="underlay"/>
class="sidebar-flexer mobile-hidden" <div id="sidebar" class="column -scrollable">
:style="sidebarAlign" <user-panel />
> <template v-if="layoutType !== 'mobile'">
<div class="sidebar-bounds"> <nav-panel />
<div class="sidebar-scroller"> <instance-specific-panel v-if="showInstanceSpecificPanel" />
<div class="sidebar"> <features-panel v-if="!currentUser && showFeaturesPanel" />
<user-panel /> <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<div v-if="!isMobileLayout"> <div id="notifs-sidebar" />
<nav-panel /> </template>
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<notifications v-if="currentUser" />
</div>
</div>
</div>
</div>
</div> </div>
<div class="main"> <div id="main-scroller" class="column main">
<div <div
v-if="!currentUser" v-if="!currentUser"
class="login-hint panel panel-default" class="login-hint panel panel-default"
@ -47,6 +40,7 @@
</div> </div>
<router-view /> <router-view />
</div> </div>
<div id="notifs-column" class="column -scrollable"/>
<media-modal /> <media-modal />
</div> </div>
<shout-panel <shout-panel

View file

@ -332,8 +332,11 @@ const checkOAuthToken = async ({ store }) => {
} }
const afterStoreSetup = async ({ store, i18n }) => { const afterStoreSetup = async ({ store, i18n }) => {
const width = windowWidth() // TODO cleanup copypasta
store.dispatch('setMobileLayout', width <= 800) const mobileLayout = windowWidth() <= 800
const wideLayout = windowWidth() >= 1300
const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
store.dispatch('setLayoutType', layoutType)
FaviconService.initFaviconService() FaviconService.initFaviconService()

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="sidebar"> <div class="column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" /> <instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel /> <staff-panel />
<terms-of-service-panel /> <terms-of-service-panel />

View file

@ -6,7 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue' import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js' import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js' import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faChevronDown, faChevronDown,
@ -19,8 +19,10 @@ library.add(
faChevronLeft faChevronLeft
) )
const scroller = () => document.getElementById('content')
const BOTTOMED_OUT_OFFSET = 10 const BOTTOMED_OUT_OFFSET = 10
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
const SAFE_RESIZE_TIME_OFFSET = 100 const SAFE_RESIZE_TIME_OFFSET = 100
const MARK_AS_READ_DELAY = 1500 const MARK_AS_READ_DELAY = 1500
const MAX_RETRIES = 10 const MAX_RETRIES = 10
@ -46,19 +48,18 @@ const Chat = {
window.addEventListener('resize', this.handleLayoutChange) window.addEventListener('resize', this.handleLayoutChange)
}, },
mounted () { mounted () {
window.addEventListener('scroll', this.handleScroll) scroller().addEventListener('scroll', this.handleScroll)
if (typeof document.hidden !== 'undefined') { if (typeof document.hidden !== 'undefined') {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false) document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
} }
this.$nextTick(() => { this.$nextTick(() => {
this.updateScrollableContainerHeight()
this.handleResize() this.handleResize()
}) })
this.setChatLayout() this.setChatLayout()
}, },
unmounted () { unmounted () {
window.removeEventListener('scroll', this.handleScroll) scroller().removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleLayoutChange) window.removeEventListener('resize', this.handleLayoutChange)
this.unsetChatLayout() this.unsetChatLayout()
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
@ -132,7 +133,6 @@ const Chat = {
onFilesDropped () { onFilesDropped () {
this.$nextTick(() => { this.$nextTick(() => {
this.handleResize() this.handleResize()
this.updateScrollableContainerHeight()
}) })
}, },
handleVisibilityChange () { handleVisibilityChange () {
@ -154,10 +154,6 @@ const Chat = {
if (html) { if (html) {
html.classList.add('chat-layout') html.classList.add('chat-layout')
} }
this.$nextTick(() => {
this.updateScrollableContainerHeight()
})
}, },
unsetChatLayout () { unsetChatLayout () {
let html = document.querySelector('html') let html = document.querySelector('html')
@ -167,17 +163,9 @@ const Chat = {
}, },
handleLayoutChange () { handleLayoutChange () {
this.$nextTick(() => { this.$nextTick(() => {
this.updateScrollableContainerHeight()
this.scrollDown() this.scrollDown()
}) })
}, },
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
updateScrollableContainerHeight () {
const header = this.$refs.header
const footer = this.$refs.footer
const inner = this.mobileLayout ? window.document.body : this.$refs.inner
this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
},
// Preserves the scroll position when OSK appears or the posting form changes its height. // Preserves the scroll position when OSK appears or the posting form changes its height.
handleResize (opts = {}) { handleResize (opts = {}) {
const { expand = false, delayed = false } = opts const { expand = false, delayed = false } = opts
@ -190,17 +178,14 @@ const Chat = {
} }
this.$nextTick(() => { this.$nextTick(() => {
this.updateScrollableContainerHeight()
const { offsetHeight = undefined } = this.lastScrollPosition const { offsetHeight = undefined } = this.lastScrollPosition
this.lastScrollPosition = getScrollPosition(this.$refs.scrollable) this.lastScrollPosition = getScrollPosition(scroller())
const diff = this.lastScrollPosition.offsetHeight - offsetHeight const diff = this.lastScrollPosition.offsetHeight - offsetHeight
if (diff < 0 || (!this.bottomedOut() && expand)) { if (diff < 0 || (!this.bottomedOut() && expand)) {
this.$nextTick(() => { this.$nextTick(() => {
this.updateScrollableContainerHeight() scroller().scrollTo({
this.$refs.scrollable.scrollTo({ top: scroller().scrollTop - diff,
top: this.$refs.scrollable.scrollTop - diff,
left: 0 left: 0
}) })
}) })
@ -209,7 +194,7 @@ const Chat = {
}, },
scrollDown (options = {}) { scrollDown (options = {}) {
const { behavior = 'auto', forceRead = false } = options const { behavior = 'auto', forceRead = false } = options
const scrollable = this.$refs.scrollable const scrollable = scroller()
if (!scrollable) { return } if (!scrollable) { return }
this.$nextTick(() => { this.$nextTick(() => {
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior }) scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
@ -228,10 +213,10 @@ const Chat = {
}) })
}, },
bottomedOut (offset) { bottomedOut (offset) {
return isBottomedOut(this.$refs.scrollable, offset) return isBottomedOut(scroller(), offset)
}, },
reachedTop () { reachedTop () {
const scrollable = this.$refs.scrollable const scrollable = scroller()
return scrollable && scrollable.scrollTop <= 0 return scrollable && scrollable.scrollTop <= 0
}, },
cullOlderCheck () { cullOlderCheck () {
@ -263,8 +248,8 @@ const Chat = {
} }
}, 200), }, 200),
handleScrollUp (positionBeforeLoading) { handleScrollUp (positionBeforeLoading) {
const positionAfterLoading = getScrollPosition(this.$refs.scrollable) const positionAfterLoading = getScrollPosition(scroller())
this.$refs.scrollable.scrollTo({ scroller().scrollTo({
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading), top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
left: 0 left: 0
}) })
@ -285,22 +270,18 @@ const Chat = {
chatService.clear(chatMessageService) chatService.clear(chatMessageService)
} }
const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable) const positionBeforeUpdate = getScrollPosition(scroller())
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => { this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
this.$nextTick(() => { this.$nextTick(() => {
if (fetchOlderMessages) { if (fetchOlderMessages) {
this.handleScrollUp(positionBeforeUpdate) this.handleScrollUp(positionBeforeUpdate)
} }
if (isFirstFetch) {
this.updateScrollableContainerHeight()
}
// In vertical screens, the first batch of fetched messages may not always take the // In vertical screens, the first batch of fetched messages may not always take the
// full height of the scrollable container. // full height of the scrollable container.
// If this is the case, we want to fetch the messages until the scrollable container // If this is the case, we want to fetch the messages until the scrollable container
// is fully populated so that the user has the ability to scroll up and load the history. // is fully populated so that the user has the ability to scroll up and load the history.
if (!isScrollable(this.$refs.scrollable) && messages.length > 0) { if (!isScrollable(scroller()) && messages.length > 0) {
this.fetchChat({ maxId: this.currentChatMessageService.minId }) this.fetchChat({ maxId: this.currentChatMessageService.minId })
} }
}) })
@ -336,9 +317,6 @@ const Chat = {
this.handleResize() this.handleResize()
// When the posting form size changes because of a media attachment, we need an extra resize // When the posting form size changes because of a media attachment, we need an extra resize
// to account for the potential delay in the DOM update. // to account for the potential delay in the DOM update.
setTimeout(() => {
this.updateScrollableContainerHeight()
}, SAFE_RESIZE_TIME_OFFSET)
this.scrollDown({ forceRead: true }) this.scrollDown({ forceRead: true })
}) })
}, },

View file

@ -1,19 +1,12 @@
.chat-view { .chat-view {
display: flex; display: flex;
height: calc(100vh - 60px); height: 100%;
width: 100%;
.chat-title {
// prevents chat header jumping on when the user avatar loads
height: 28px;
}
.chat-view-inner { .chat-view-inner {
height: auto; height: auto;
width: 100%; width: 100%;
overflow: visible; overflow: visible;
display: flex; display: flex;
margin: 0.5em 0.5em 0 0.5em;
} }
.chat-view-body { .chat-view-body {
@ -32,11 +25,9 @@
} }
} }
.scrollable-message-list { .message-list {
padding: 0 0.8em; padding: 0 0.8em;
height: 100%; height: 100%;
overflow-y: scroll;
overflow-x: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -44,12 +35,14 @@
.footer { .footer {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
z-index: 10;
} }
.chat-view-heading { .chat-view-heading {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
top: 50px;
display: flex; display: flex;
z-index: 2; z-index: 2;
position: sticky; position: sticky;

View file

@ -8,7 +8,7 @@
> >
<div <div
ref="header" ref="header"
class="panel-heading chat-view-heading mobile-hidden" class="panel-heading -sticky chat-view-heading mobile-hidden"
> >
<a <a
class="go-back-button" class="go-back-button"
@ -27,10 +27,8 @@
</div> </div>
</div> </div>
<div <div
ref="scrollable" class="message-list"
class="scrollable-message-list"
:style="{ height: scrollableContainerHeight }" :style="{ height: scrollableContainerHeight }"
@scroll="handleScroll"
> >
<template v-if="!errorLoadingChat"> <template v-if="!errorLoadingChat">
<ChatMessage <ChatMessage

View file

@ -6,7 +6,7 @@
v-else v-else
class="chat-list panel panel-default" class="chat-list panel panel-default"
> >
<div class="panel-heading"> <div class="panel-heading -sticky">
<span class="title"> <span class="title">
{{ $t("chats.chats") }} {{ $t("chats.chats") }}
</span> </span>

View file

@ -15,7 +15,7 @@
</router-link> </router-link>
<RichContent <RichContent
class="username" class="username"
:title="'@'+user.screen_name_ui" :title="'@'+(user && user.screen_name_ui)"
:html="htmlTitle" :html="htmlTitle"
:emoji="user.emoji" :emoji="user.emoji"
/> />

View file

@ -1,9 +1,7 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.DesktopNav { .DesktopNav {
height: 50px;
width: 100%; width: 100%;
position: fixed;
a { a {
color: var(--topBarLink, $fallback--link); color: var(--topBarLink, $fallback--link);

View file

@ -78,7 +78,8 @@ const MobileNav = {
this.$store.dispatch('logout') this.$store.dispatch('logout')
}, },
markNotificationsAsSeen () { markNotificationsAsSeen () {
this.$refs.notifications.markAsSeen() // this.$refs.notifications.markAsSeen()
this.$store.dispatch('markNotificationsAsSeen')
}, },
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
if (scrollTop + clientHeight >= scrollHeight) { if (scrollTop + clientHeight >= scrollHeight) {

View file

@ -69,12 +69,9 @@
</div> </div>
<div <div
class="mobile-notifications" class="mobile-notifications"
id="mobile-notifications"
@scroll="onScroll" @scroll="onScroll"
> >
<Notifications
ref="notifications"
:no-heading="true"
/>
</div> </div>
</div> </div>
<SideDrawer <SideDrawer
@ -92,12 +89,10 @@
.MobileNav { .MobileNav {
.mobile-nav { .mobile-nav {
display: grid; display: grid;
line-height: 50px; line-height: var(--navbar-height);
height: 50px;
grid-template-rows: 50px; grid-template-rows: 50px;
grid-template-columns: 2fr auto; grid-template-columns: 2fr auto;
width: 100%; width: 100%;
position: fixed;
box-sizing: border-box; box-sizing: border-box;
} }
@ -182,7 +177,7 @@
.mobile-notifications { .mobile-notifications {
margin-top: 50px; margin-top: 50px;
width: 100vw; width: 100vw;
height: calc(100vh - 50px); height: calc(100vh - var(--navbar-height));
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;

View file

@ -23,8 +23,6 @@ const Notifications = {
NotificationFilters NotificationFilters
}, },
props: { props: {
// Disables display of panel header
noHeading: Boolean,
// Disables panel styles, unread mark, potentially other notification-related actions // Disables panel styles, unread mark, potentially other notification-related actions
// meant for "Interactions" timeline // meant for "Interactions" timeline
minimalMode: Boolean, minimalMode: Boolean,
@ -65,6 +63,19 @@ const Notifications = {
loading () { loading () {
return this.$store.state.statuses.notifications.loading return this.$store.state.statuses.notifications.loading
}, },
noHeading () {
const { layoutType } = this.$store.state.interface
console.log(layoutType)
return layoutType === 'mobile'
},
teleportTarget () {
const { layoutType } = this.$store.state.interface
const map = {
wide: '#notifs-column',
mobile: '#mobile-notifications'
}
return map[layoutType] || '#notifs-sidebar'
},
notificationsToDisplay () { notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}, },

View file

@ -1,69 +1,71 @@
<template> <template>
<div <teleport :to="teleportTarget">
:class="{ minimal: minimalMode }" <div
class="Notifications" :class="{ minimal: minimalMode }"
> class="Notifications"
<div :class="mainClass"> >
<div <div :class="mainClass">
v-if="!noHeading"
class="panel-heading"
>
<div class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCount"
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
</div>
<button
v-if="unseenCount"
class="button-default read-button"
@click.prevent="markAsSeen"
>
{{ $t('notifications.read') }}
</button>
<NotificationFilters />
</div>
<div class="panel-body">
<div <div
v-for="notification in notificationsToDisplay" v-if="!noHeading"
:key="notification.id" class="notifications-heading panel-heading -sticky"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
> >
<div class="notification-overlay" /> <div class="title">
<notification :notification="notification" /> {{ $t('notifications.notifications') }}
</div> <span
</div> v-if="unseenCount"
<div class="panel-footer notifications-footer"> class="badge badge-notification unseen-count"
<div >{{ unseenCount }}</span>
v-if="bottomedOut" </div>
class="new-status-notification text-center faint" <button
> v-if="unseenCount"
{{ $t('notifications.no_more_notifications') }} class="button-default read-button"
</div> @click.prevent="markAsSeen"
<button >
v-else-if="!loading" {{ $t('notifications.read') }}
class="button-unstyled -link -fullwidth" </button>
@click.prevent="fetchOlderNotifications()" <NotificationFilters />
> </div>
<div class="new-status-notification text-center"> <div class="panel-body">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }} <div
v-for="notification in notificationsToDisplay"
:key="notification.id"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
>
<div class="notification-overlay" />
<notification :notification="notification" />
</div>
</div>
<div class="panel-footer notifications-footer">
<div
v-if="bottomedOut"
class="new-status-notification text-center faint"
>
{{ $t('notifications.no_more_notifications') }}
</div>
<button
v-else-if="!loading"
class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderNotifications()"
>
<div class="new-status-notification text-center">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
</div>
</button>
<div
v-else
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div> </div>
</button>
<div
v-else
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </teleport>
</template> </template>
<script src="./notifications.js"></script> <script src="./notifications.js"></script>

View file

@ -91,6 +91,9 @@
flex-direction: column; flex-direction: column;
margin-top: 0.6em; margin-top: 0.6em;
max-width: 18rem; max-width: 18rem;
> * {
min-width: 0;
}
} }
.form-group { .form-group {

View file

@ -149,5 +149,30 @@
} }
} }
.button-default.dropdown-item {
&,
i[class*=icon-] {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
&:active {
background-color: $fallback--lightBg;
background-color: var(--selectedMenuPopover, $fallback--lightBg);
color: $fallback--link;
color: var(--selectedMenuPopoverText, $fallback--link);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
}
}
} }
</style> </style>

View file

@ -271,7 +271,10 @@ $validations-cRed: #f04124;
.container { .container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
//margin-bottom: 1em;
> * {
min-width: 0;
}
} }
.terms-of-service { .terms-of-service {

View file

@ -2,6 +2,18 @@
.settings-modal { .settings-modal {
overflow: hidden; overflow: hidden;
.setting-list,
.option-list {
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em
}
}
&.peek { &.peek {
.settings-modal-panel { .settings-modal-panel {
/* Explanation: /* Explanation:

View file

@ -98,7 +98,7 @@
.icon { .icon {
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--panelText, $fallback--text);
margin-right: 0.5em; margin-right: 0.5em;
} }

View file

@ -49,7 +49,7 @@ const SideDrawer = {
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
shout () { return this.$store.state.shout.channel.state === 'joined' }, shout () { return this.$store.state.shout.joined },
unseenNotifications () { unseenNotifications () {
return unseenNotificationsFromStore(this.$store) return unseenNotificationsFromStore(this.$store)
}, },

View file

@ -42,6 +42,10 @@
display: flex; display: flex;
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
> * {
min-width: 0;
}
&.-repeat { &.-repeat {
padding-top: 0; padding-top: 0;
} }

View file

@ -64,7 +64,7 @@ const Timeline = {
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return { return {
root: rootClasses, root: rootClasses,
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []), header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []),
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []), body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : []) footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
} }
@ -89,7 +89,7 @@ const Timeline = {
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0 const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.handleScroll) document.getElementById('content').addEventListener('scroll', this.handleScroll)
if (store.state.api.fetchers[this.timelineName]) { return false } if (store.state.api.fetchers[this.timelineName]) { return false }
@ -111,7 +111,7 @@ const Timeline = {
setTimeout(this.determineVisibleStatuses, 250) setTimeout(this.determineVisibleStatuses, 250)
}, },
unmounted () { unmounted () {
window.removeEventListener('scroll', this.handleScroll) document.getElementById('content').removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey) window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) this.$store.commit('setLoading', { timeline: this.timelineName, value: false })

View file

@ -13,7 +13,6 @@
max-width: 100%; max-width: 100%;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
position: relative;
.loadmore-button { .loadmore-button {
flex-shrink: 0; flex-shrink: 0;

View file

@ -8,7 +8,7 @@
:style="style" :style="style"
class="background-image" class="background-image"
/> />
<div class="panel-heading"> <div class="panel-heading -flexible-height">
<div class="user-info"> <div class="user-info">
<div class="container"> <div class="container">
<a <a
@ -289,6 +289,7 @@
.user-card { .user-card {
position: relative; position: relative;
z-index: 1;
&:hover { &:hover {
--_still-image-img-visibility: visible; --_still-image-img-visibility: visible;
@ -331,6 +332,7 @@
border-top-left-radius: calc(var(--panelRadius) - 1px); border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px); border-top-right-radius: calc(var(--panelRadius) - 1px);
background-color: var(--profileBg); background-color: var(--profileBg);
z-index: -2;
&.hide-bio { &.hide-bio {
mask-size: 100% 40px; mask-size: 100% 40px;
@ -385,11 +387,16 @@
padding: 0 26px; padding: 0 26px;
.container { .container {
min-width: 0;
padding: 16px 0 6px; padding: 16px 0 6px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
max-height: 56px; max-height: 56px;
> * {
min-width: 0;
}
.Avatar { .Avatar {
--_avatarShadowBox: var(--avatarShadow); --_avatarShadowBox: var(--avatarShadow);
--_avatarShadowFilter: var(--avatarShadowFilter); --_avatarShadowFilter: var(--avatarShadowFilter);

View file

@ -32,7 +32,7 @@ const loaders = {
pt: () => import('./pt.json'), pt: () => import('./pt.json'),
ro: () => import('./ro.json'), ro: () => import('./ro.json'),
ru: () => import('./ru.json'), ru: () => import('./ru.json'),
// te: () => import('./te.json'), // buggy te: () => import('./te.json'),
uk: () => import('./uk.json'), uk: () => import('./uk.json'),
zh: () => import('./zh.json'), zh: () => import('./zh.json'),
zh_Hant: () => import('./zh_Hant.json') zh_Hant: () => import('./zh_Hant.json')

View file

@ -49,7 +49,7 @@
"notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు", "notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు",
"notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు", "notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు",
"post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి", "post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి",
"post_status.account_not_locked_warning": "మీ ఖాతా {} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.", "post_status.account_not_locked_warning": "మీ ఖాతా {0} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.",
"post_status.account_not_locked_warning_link": "తాళం వేయబడినది", "post_status.account_not_locked_warning_link": "తాళం వేయబడినది",
"post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి", "post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి",
"post_status.content_type.text/plain": "సాధారణ అక్షరాలు", "post_status.content_type.text/plain": "సాధారణ అక్షరాలు",

View file

@ -13,7 +13,7 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
) )
}, },
mobileLayout: false, layoutType: 'normal',
globalNotices: [], globalNotices: [],
layoutHeight: 0, layoutHeight: 0,
lastTimeline: null lastTimeline: null
@ -36,8 +36,8 @@ const interfaceMod = {
setNotificationPermission (state, permission) { setNotificationPermission (state, permission) {
state.notificationPermission = permission state.notificationPermission = permission
}, },
setMobileLayout (state, value) { setLayoutType (state, value) {
state.mobileLayout = value state.layoutType = value
}, },
closeSettingsModal (state) { closeSettingsModal (state) {
state.settingsModalState = 'hidden' state.settingsModalState = 'hidden'
@ -86,8 +86,8 @@ const interfaceMod = {
setNotificationPermission ({ commit }, permission) { setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission) commit('setNotificationPermission', permission)
}, },
setMobileLayout ({ commit }, value) { setLayoutType ({ commit }, value) {
commit('setMobileLayout', value) commit('setLayoutType', value)
}, },
closeSettingsModal ({ commit }) { closeSettingsModal ({ commit }) {
commit('closeSettingsModal') commit('closeSettingsModal')

View file

@ -1,7 +1,8 @@
const shout = { const shout = {
state: { state: {
messages: [], messages: [],
channel: { state: '' } channel: { state: '' },
joined: false
}, },
mutations: { mutations: {
setChannel (state, channel) { setChannel (state, channel) {
@ -13,11 +14,23 @@ const shout = {
}, },
setMessages (state, messages) { setMessages (state, messages) {
state.messages = messages.slice(-19, 20) state.messages = messages.slice(-19, 20)
},
setJoined (state, joined) {
state.joined = joined
} }
}, },
actions: { actions: {
initializeShout (store, socket) { initializeShout (store, socket) {
const channel = socket.channel('chat:public') const channel = socket.channel('chat:public')
channel.joinPush.receive('ok', () => {
store.commit('setJoined', true)
})
channel.onClose(() => {
store.commit('setJoined', false)
})
channel.onError(() => {
store.commit('setJoined', false)
})
channel.on('new_msg', (msg) => { channel.on('new_msg', (msg) => {
store.commit('addMessage', msg) store.commit('addMessage', msg)
}) })

204
src/panel.scss Normal file
View file

@ -0,0 +1,204 @@
.panel {
position: relative;
display: flex;
flex-direction: column;
z-index: 0;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::after,
& {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
pointer-events: none;
}
}
.panel-body:empty::before {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
margin: 1em;
text-align: center;
}
.panel-heading {
position: relative;
box-sizing: border-box;
display: flex;
flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
color: var(--panelText);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
align-items: baseline;
height: var(--panelHeadingHeight);
z-index: -2;
--panelHeadingHeight: 45px;
&.-flexible-height {
--panelHeadingHeight: auto;
&::after,
&::before {
display: none;
}
}
&.-sticky {
position: sticky;
top: 0;
z-index: 2;
}
&::after,
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
pointer-events: none;
}
&::after {
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
z-index: -2;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
box-shadow: var(--panelHeaderShadow);
}
&::before {
bottom: -20px;
z-index: -1;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
mask: linear-gradient(to bottom, white var(--panelHeadingHeight), transparent var(--panelHeadingHeight));
}
.title {
flex: 1 0 auto;
font-size: 1.3em;
}
.faint {
background-color: transparent;
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
.button-default,
.alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: 0.5em;
min-width: 1px;
align-self: stretch;
}
.button-default {
flex-shrink: 0;
&,
i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedPanel, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedPanelText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledPanelText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledPanelText, $fallback--text);
}
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link);
}
}
.panel-heading.stub {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
/* TODO Should remove timeline-footer from here when we refactor panels into
* separate component and utilize slots
*/
.panel-footer {
display: flex;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
flex: none;
padding: 0.6em 0.6em;
text-align: left;
line-height: 28px;
align-items: baseline;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
a,
.-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link);
}
}
.panel-body > p {
line-height: 18px;
padding: 1em;
margin: 0;
}