diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7f6d3c92..85d3ee44 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 00000000..fbf3a245 --- /dev/null +++ b/.stylelintrc.json @@ -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+" + } + ] + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 887588f3..18dafa8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,30 +2,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + ## [Unreleased] +## [Unreleased patch] + ### Changed -- Greentext now has separate color slot for it -- Removed the use of with_move parameters when fetching notifications -- Push notifications now are the same as normal notfication, and are localized. +- Polls will be hidden with status content if "Collapse posts with subjects" is enabled and the post is collapsed. ### Fixed -- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully) -- Multiple issues with muted statuses/notifications +- Autocomplete won't stop at the second @, so it'll still work with "@lain@l" and not start over. +- Fixed weird autocomplete behavior when you write ":custom_emoji: ?" -## [Unreleased patch] -### Add -- Added private notifications option for push notifications -- 'Copy link' button for statuses (in the ellipsis menu) +## [2.1.0] - 2020-08-28 +### Added - Autocomplete domains from list of known instances - 'Bot' settings option and badge - Added profile meta data fields that can be set in profile settings +- Added option to reset avatar/banner in profile settings +- Descriptions can be set on uploaded files before posting +- Added status preview option to preview your statuses before posting +- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style +- Added ability to see all favoriting or repeating users when hovering the number on highlighted statuses +- Bookmarks ### Changed -- Registration page no longer requires email if the server is configured not to require it - Change heart to thumbs up in reaction picker - Close the media modal on navigation events - Add colons to the emoji alt text, to make them copyable - Add better visual indication for drag-and-drop for files +- When disabling attachments, the placeholder links now show an icon and the description instead of just IMAGE or VIDEO etc +- Remove unnecessary options for 'automatic loading when loading older' and 'reply previews' +- Greentext now has separate color slot for it +- Removed the use of with_move parameters when fetching notifications +- Push notifications now are the same as normal notfication, and are localized. +- Updated Notification Settings to match new BE API ### Fixed - Custom Emoji will display in poll options now. @@ -37,6 +47,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Subject field now appears disabled when posting - Fix status ellipsis menu being cut off in notifications column - Fixed autocomplete sometimes not returning the right user when there's already some results +- Videos and audio and misc files show description as alt/title properly now +- Clicking on non-image/video files no longer opens an empty modal +- Audio files can now be played back in the frontend with hidden attachments +- Videos are not cropped awkwardly in the uploads section anymore +- Reply filtering options in Settings -> Filtering now work again using filtering on server +- Don't show just blank-screen when cookies are disabled +- Add status idempotency to prevent accidental double posting when posting returns an error +- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully) +- Multiple issues with muted statuses/notifications + +## [2.0.5] - 2020-05-12 +### Added +- Added private notifications option for push notifications +- 'Copy link' button for statuses (in the ellipsis menu) + +### Changed +- Registration page no longer requires email if the server is configured not to require it + +### Fixed +- Status ellipsis menu closes properly when selecting certain options ## [2.0.3] - 2020-05-02 ### Fixed @@ -46,7 +76,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach: -### Add +### Added - Follow request notification support ## [2.0.2] - 2020-04-08 @@ -98,6 +128,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ability to change user's email - About page - Added remote user redirect +- Bookmarks ### Changed - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes ### Fixed diff --git a/README.md b/README.md index b66383ad..54529a70 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f417f33d..241ad331 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -8,8 +8,6 @@ > > --Catbag -Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. - ## Posting, reading, basic functions. After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..8764f9ab --- /dev/null +++ b/docs/index.md @@ -0,0 +1,8 @@ +# Introduction to Pleroma-FE +## What is Pleroma-FE? + +Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. + +## How can I use it? + +If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. ). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md). diff --git a/package.json b/package.json index c0665f6e..75d9ee56 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -23,6 +24,7 @@ "diff": "^3.0.1", "escape-html": "^1.0.3", "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", @@ -35,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", @@ -79,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", @@ -100,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", diff --git a/src/App.js b/src/App.js index 040138c9..ded772fa 100644 --- a/src/App.js +++ b/src/App.js @@ -13,7 +13,8 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil import MobileNav from './components/mobile_nav/mobile_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' -import { windowWidth } from './services/window_utils/window_utils' +import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' +import { windowWidth, windowHeight } from './services/window_utils/window_utils' export default { name: 'app', @@ -32,7 +33,8 @@ export default { MobileNav, SettingsModal, UserReportingModal, - PostStatusModal + PostStatusModal, + GlobalNoticeList }, data: () => ({ mobileActivePanel: 'timeline', @@ -125,10 +127,12 @@ export default { }, updateMobileState () { const mobileLayout = windowWidth() <= 800 + const layoutHeight = windowHeight() const changed = mobileLayout !== this.isMobileLayout if (changed) { this.$store.dispatch('setMobileLayout', mobileLayout) } + this.$store.dispatch('setLayoutHeight', layoutHeight) } } } diff --git a/src/App.scss b/src/App.scss index f2972eda..e2e2d079 100644 --- a/src/App.scss +++ b/src/App.scss @@ -47,6 +47,7 @@ html { } body { + overscroll-behavior-y: none; font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; @@ -319,7 +320,7 @@ option { i[class*=icon-] { color: $fallback--icon; - color: var(--icon, $fallback--icon) + color: var(--icon, $fallback--icon); } .btn-block { @@ -858,6 +859,10 @@ nav { display: block; margin-right: 0.8em; } + + .main { + margin-bottom: 7em; + } } .select-multiple { @@ -924,3 +929,51 @@ nav { background-color: $fallback--fg; background-color: var(--panel, $fallback--fg); } + +.unread-chat-count { + font-size: 0.9em; + font-weight: bolder; + font-style: normal; + position: absolute; + right: 0.6rem; + padding: 0 0.3em; + min-width: 1.3rem; + min-height: 1.3rem; + max-height: 1.3rem; + line-height: 1.3rem; +} + +.chat-layout { + // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). + overflow: hidden; + height: 100%; + + // Ensures the fixed position of the mobile browser bars on scroll up / down events. + // Prevents the mobile browser bars from overlapping or hiding the message posting form. + @media all and (max-width: 800px) { + body { + height: 100%; + } + + #app { + height: 100%; + overflow: hidden; + min-height: auto; + } + + #app_bg_wrapper { + overflow: hidden; + } + + .main { + overflow: hidden; + height: 100%; + } + + #content { + padding-top: 0; + height: 100%; + overflow: visible; + } + } +} diff --git a/src/App.vue b/src/App.vue index 7b9ad3dc..0276c6a6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -77,6 +77,7 @@ +
- - - +
@@ -128,6 +127,7 @@ + diff --git a/src/_variables.scss b/src/_variables.scss index 30dc3e42..9004d551 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -27,5 +27,6 @@ $fallback--tooltipRadius: 5px; $fallback--avatarRadius: 4px; $fallback--avatarAltRadius: 10px; $fallback--attachmentRadius: 10px; +$fallback--chatMessageRadius: 10px; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 1796eb1b..00ca74a2 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -20,15 +20,23 @@ const parsedInitialResults = () => { return staticInitialResults } +const decodeUTF8Base64 = (data) => { + const rawData = atob(data) + const array = Uint8Array.from([...rawData].map((char) => char.charCodeAt(0))) + const text = new TextDecoder().decode(array) + return text +} + const preloadFetch = async (request) => { const data = parsedInitialResults() if (!data || !data[request]) { return window.fetch(request) } - const requestData = atob(data[request]) + const decoded = decodeUTF8Base64(data[request]) + const requestData = JSON.parse(decoded) return { ok: true, - json: () => JSON.parse(requestData), + json: () => requestData, text: () => requestData } } @@ -215,7 +223,6 @@ const getAppSecret = async ({ store }) => { const resolveStaffAccounts = ({ store, accounts }) => { const nicknames = accounts.map(uri => uri.split('/').pop()) - nicknames.map(nickname => store.dispatch('fetchUser', nickname)) store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) } @@ -231,6 +238,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) diff --git a/src/boot/routes.js b/src/boot/routes.js index d98a3b50..b5d3c631 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -2,9 +2,12 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue' import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue' import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' import TagTimeline from 'components/tag_timeline/tag_timeline.vue' +import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue' import ConversationPage from 'components/conversation-page/conversation-page.vue' import Interactions from 'components/interactions/interactions.vue' import DMs from 'components/dm_timeline/dm_timeline.vue' +import ChatList from 'components/chat_list/chat_list.vue' +import Chat from 'components/chat/chat.vue' import UserProfile from 'components/user_profile/user_profile.vue' import Search from 'components/search/search.vue' import Registration from 'components/registration/registration.vue' @@ -27,7 +30,7 @@ export default (store) => { } } - return [ + let routes = [ { name: 'root', path: '/', redirect: _to => { @@ -40,6 +43,7 @@ export default (store) => { { name: 'public-timeline', path: '/main/public', component: PublicTimeline }, { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, + { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'remote-user-profile-acct', path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)', @@ -60,11 +64,20 @@ export default (store) => { { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, - { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, + { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, { name: 'user-profile', path: '/(users/)?:name', component: UserProfile } ] + + if (store.state.instance.pleromaChatMessagesAvailable) { + routes = routes.concat([ + { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }, + { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute } + ]) + } + + return routes } diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 0826c275..6d345bc7 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,3 +1,4 @@ +import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' @@ -27,7 +28,18 @@ const AccountActions = { }, reportUser () { this.$store.dispatch('openUserReportingModal', this.user.id) + }, + openChat () { + this.$router.push({ + name: 'chat', + params: { recipient_id: this.user.id } + }) } + }, + computed: { + ...mapState({ + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + }) } } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 029e7096..987e94b7 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -50,6 +50,13 @@ > {{ $t('user_card.report') }} +
@@ -83,6 +90,8 @@
@@ -84,54 +84,56 @@ max-width: 25em; } -.chat-heading { - cursor: pointer; - .icon-comment-empty { - color: $fallback--text; - color: var(--text, $fallback--text); - } -} - -.chat-window { - overflow-y: auto; - overflow-x: hidden; - max-height: 20em; -} - -.chat-window-container { - height: 100%; -} - -.chat-message { - display: flex; - padding: 0.2em 0.5em -} - -.chat-avatar { - img { - height: 24px; - width: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); - margin-right: 0.5em; - margin-top: 0.25em; - } -} - -.chat-input { - display: flex; - textarea { - flex: 1; - margin: 0.6em; - min-height: 3.5em; - resize: none; - } -} - .chat-panel { - .title { + .chat-heading { + cursor: pointer; + .icon-comment-empty { + color: $fallback--text; + color: var(--text, $fallback--text); + } + } + + .chat-window { + overflow-y: auto; + overflow-x: hidden; + max-height: 20em; + } + + .chat-window-container { + height: 100%; + } + + .chat-message { display: flex; - justify-content: space-between; + padding: 0.2em 0.5em + } + + .chat-avatar { + img { + height: 24px; + width: 24px; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + margin-right: 0.5em; + margin-top: 0.25em; + } + } + + .chat-input { + display: flex; + textarea { + flex: 1; + margin: 0.6em; + min-height: 3.5em; + resize: none; + } + } + + .chat-panel { + .title { + display: flex; + justify-content: space-between; + } } } diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js new file mode 100644 index 00000000..e424bb1f --- /dev/null +++ b/src/components/chat_title/chat_title.js @@ -0,0 +1,26 @@ +import Vue from 'vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import UserAvatar from '../user_avatar/user_avatar.vue' + +export default Vue.component('chat-title', { + name: 'ChatTitle', + components: { + UserAvatar + }, + props: [ + 'user', 'withAvatar' + ], + computed: { + title () { + return this.user ? this.user.screen_name : '' + }, + htmlTitle () { + return this.user ? this.user.name_html : '' + } + }, + methods: { + getUserProfileLink (user) { + return generateProfileLink(user.id, user.screen_name) + } + } +}) diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue new file mode 100644 index 00000000..b16ed39d --- /dev/null +++ b/src/components/chat_title/chat_title.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index 03375b2f..d28c2cfd 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -52,7 +52,7 @@ export default { right: 0; top: 0; display: block; - content: '✔'; + content: '✓'; transition: color 200ms; width: 1.1em; height: 1.1em; diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 2e48240a..997a4d10 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,7 +1,7 @@