diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e4bf822..dd4a2836 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Added
+- Mouseover titles for emojis in reaction picker
+- Support to input emoji into the search box in reaction picker
+- Added some missing unicode emoji
+
### Fixed
- Fixed the occasional bug where screen would scroll 1px when typing into a reply form
+- Fixed timeline errors locking timelines
+- Fixed missing highlighted border in expanded conversations
- Fixed custom emoji not working in profile field names
+- Fixed pinned statuses not appearing in user profiles
+- Fixed some elements not being keyboard navigation friendly
+- Fixed your latest chat messages disappearing when closing chat view and opening it again during the same session
+
+### Changed
+- Errors when fetching are now shown with popup errors instead of "Error fetching updates" in panel headers
+- Made reply/fav/repeat etc buttons easier to hit
## [2.2.1] - 2020-11-11
@@ -22,6 +36,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Import/export a muted users
- Proper handling of deletes when using websocket streaming
- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent
+- Added a small red badge to the favicon when there's unread notifications
+- Added the NSFW alert to link previews
### Fixed
- Fixed clicking NSFW hider through status popover
@@ -43,7 +59,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.1.2] - 2020-09-17
### Fixed
- Fixed chats list not updating its order when new messages come in
-- Fixed chat messages sometimes getting lost when you receive a message at the same time
+- Fixed chat messages sometimes getting lost when you receive a message at the same time
## [2.1.1] - 2020-09-08
diff --git a/src/App.scss b/src/App.scss
index ca7d33cd..cdc3209c 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -33,6 +33,7 @@ h4 {
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));
@@ -69,7 +70,7 @@ a {
color: var(--link, $fallback--link);
}
-button {
+.button-default {
user-select: none;
color: $fallback--text;
color: var(--btnText, $fallback--text);
@@ -85,7 +86,8 @@ button {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
- i[class*=icon-], .svg-inline--fa {
+ i[class*=icon-],
+ .svg-inline--fa {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
@@ -107,7 +109,8 @@ button {
background-color: $fallback--fg;
background-color: var(--btnPressed, $fallback--fg);
- svg, i {
+ svg,
+ i {
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
}
@@ -120,7 +123,8 @@ button {
background-color: $fallback--fg;
background-color: var(--btnDisabled, $fallback--fg);
- svg, i {
+ svg,
+ i {
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
}
@@ -134,7 +138,8 @@ button {
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: var(--buttonPressedShadow);
- svg, i {
+ svg,
+ i {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
}
@@ -149,6 +154,30 @@ button {
}
}
+.button-unstyled {
+ background: none;
+ border: none;
+ outline: none;
+ display: inline;
+ text-align: initial;
+ font-size: 100%;
+ font-family: inherit;
+ padding: 0;
+ line-height: unset;
+ cursor: pointer;
+ box-sizing: content-box;
+ color: inherit;
+
+ &.-link {
+ color: $fallback--link;
+ color: var(--link, $fallback--link);
+ }
+
+ &.-fullwidth {
+ width: 100%;
+ }
+}
+
input, textarea, .select, .input {
&.unstyled {
@@ -442,6 +471,7 @@ main-router {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
+
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
@@ -453,11 +483,8 @@ main-router {
overflow-x: hidden;
}
- button {
- flex-shrink: 0;
- }
-
- button, .alert {
+ .button-default,
+ .alert {
// height: 100%;
line-height: 21px;
min-height: 0;
@@ -468,8 +495,11 @@ main-router {
align-self: stretch;
}
- button {
- &, i[class*=icon-] {
+ .button-default {
+ flex-shrink: 0;
+
+ &,
+ i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
@@ -492,7 +522,8 @@ main-router {
}
}
- a {
+ a,
+ .-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
}
@@ -507,15 +538,15 @@ main-router {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
-
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
- a {
+ a,
+ .-link {
color: $fallback--link;
- color: var(--panelLink, $fallback--link)
+ color: var(--panelLink, $fallback--link);
}
}
@@ -797,7 +828,7 @@ nav {
}
}
-.btn.btn-default {
+.btn.button-default {
min-height: 28px;
}
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 3cbbf020..b472fcf6 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -7,6 +7,7 @@ import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js'
+import FaviconService from '../services/favicon_service/favicon_service.js'
let staticInitialResults = null
@@ -326,6 +327,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800)
+ FaviconService.initFaviconService()
+
const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index e3ae376e..ab5d1d29 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -4,6 +4,7 @@
trigger="click"
placement="bottom"
:bound-to="{ x: 'container' }"
+ remove-padding
>
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
index 5af744a3..e4351d3b 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -31,9 +31,6 @@
color: $fallback--text;
color: var(--text, $fallback--text);
}
-
- border-radius: $fallback--chatMessageRadius;
- border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
}
.popover {
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index 3849ab6e..0777f880 100644
--- a/src/components/chat_message/chat_message.vue
+++ b/src/components/chat_message/chat_message.vue
@@ -53,7 +53,7 @@
-
-
+
-
+
+
+
+ >
+
+
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
index 97aee243..3b5aec14 100644
--- a/src/components/domain_mute_card/domain_mute_card.vue
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -6,7 +6,7 @@
{{ $t('domain_mute_card.unmute') }}
@@ -16,7 +16,7 @@
{{ $t('domain_mute_card.mute') }}
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 87303d08..2068a598 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -114,7 +114,8 @@ const EmojiInput = {
showPicker: false,
temporarilyHideSuggestions: false,
keepOpen: false,
- disableClickOutside: false
+ disableClickOutside: false,
+ suggestions: []
}
},
components: {
@@ -124,21 +125,6 @@ const EmojiInput = {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
- suggestions () {
- const firstchar = this.textAtCaret.charAt(0)
- if (this.textAtCaret === firstchar) { return [] }
- const matchedSuggestions = this.suggest(this.textAtCaret)
- if (matchedSuggestions.length <= 0) {
- return []
- }
- return take(matchedSuggestions, 5)
- .map(({ imageUrl, ...rest }, index) => ({
- ...rest,
- // eslint-disable-next-line camelcase
- img: imageUrl || '',
- highlighted: index === this.highlighted
- }))
- },
showSuggestions () {
return this.focused &&
this.suggestions &&
@@ -188,6 +174,23 @@ const EmojiInput = {
watch: {
showSuggestions: function (newValue) {
this.$emit('shown', newValue)
+ },
+ textAtCaret: async function (newWord) {
+ const firstchar = newWord.charAt(0)
+ this.suggestions = []
+ if (newWord === firstchar) return
+ const matchedSuggestions = await this.suggest(newWord)
+ // Async: cancel if textAtCaret has changed during wait
+ if (this.textAtCaret !== newWord) return
+ if (matchedSuggestions.length <= 0) return
+ this.suggestions = take(matchedSuggestions, 5)
+ .map(({ imageUrl, ...rest }) => ({
+ ...rest,
+ img: imageUrl || ''
+ }))
+ },
+ suggestions (newValue) {
+ this.$nextTick(this.resize)
}
},
methods: {
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 224e72cf..4becdc41 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -6,13 +6,13 @@
>
-
-
+
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 8330345b..14a2b41e 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -1,4 +1,3 @@
-import { debounce } from 'lodash'
/**
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
@@ -11,19 +10,19 @@ import { debounce } from 'lodash'
* doesn't support user linking you can just provide only emoji.
*/
-const debounceUserSearch = debounce((data, input) => {
- data.updateUsersList(input)
-}, 500)
-
-export default data => input => {
- const firstChar = input[0]
- if (firstChar === ':' && data.emoji) {
- return suggestEmoji(data.emoji)(input)
+export default data => {
+ const emojiCurry = suggestEmoji(data.emoji)
+ const usersCurry = data.store && suggestUsers(data.store)
+ return input => {
+ const firstChar = input[0]
+ if (firstChar === ':' && data.emoji) {
+ return emojiCurry(input)
+ }
+ if (firstChar === '@' && usersCurry) {
+ return usersCurry(input)
+ }
+ return []
}
- if (firstChar === '@' && data.users) {
- return suggestUsers(data)(input)
- }
- return []
}
export const suggestEmoji = emojis => input => {
@@ -57,50 +56,75 @@ export const suggestEmoji = emojis => input => {
})
}
-export const suggestUsers = data => input => {
- const noPrefix = input.toLowerCase().substr(1)
- const users = data.users
+export const suggestUsers = ({ dispatch, state }) => {
+ // Keep some persistent values in closure, most importantly for the
+ // custom debounce to work. Lodash debounce does not return a promise.
+ let suggestions = []
+ let previousQuery = ''
+ let timeout = null
+ let cancelUserSearch = null
- const newUsers = users.filter(
- user =>
- user.screen_name.toLowerCase().startsWith(noPrefix) ||
- user.name.toLowerCase().startsWith(noPrefix)
-
- /* taking only 20 results so that sorting is a bit cheaper, we display
- * only 5 anyway. could be inaccurate, but we ideally we should query
- * backend anyway
- */
- ).slice(0, 20).sort((a, b) => {
- let aScore = 0
- let bScore = 0
-
- // Matches on screen name (i.e. user@instance) makes a priority
- aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
- bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
-
- // Matches on name takes second priority
- aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
- bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
-
- const diff = (bScore - aScore) * 10
-
- // Then sort alphabetically
- const nameAlphabetically = a.name > b.name ? 1 : -1
- const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
-
- return diff + nameAlphabetically + screenNameAlphabetically
- /* eslint-disable camelcase */
- }).map(({ screen_name, name, profile_image_url_original }) => ({
- displayText: screen_name,
- detailText: name,
- imageUrl: profile_image_url_original,
- replacement: '@' + screen_name + ' '
- }))
-
- // BE search users to get more comprehensive results
- if (data.updateUsersList) {
- debounceUserSearch(data, noPrefix)
+ const userSearch = (query) => dispatch('searchUsers', { query })
+ const debounceUserSearch = (query) => {
+ cancelUserSearch && cancelUserSearch()
+ return new Promise((resolve, reject) => {
+ timeout = setTimeout(() => {
+ userSearch(query).then(resolve).catch(reject)
+ }, 300)
+ cancelUserSearch = () => {
+ clearTimeout(timeout)
+ resolve([])
+ }
+ })
+ }
+
+ return async input => {
+ const noPrefix = input.toLowerCase().substr(1)
+ if (previousQuery === noPrefix) return suggestions
+
+ suggestions = []
+ previousQuery = noPrefix
+ // Fetch more and wait, don't fetch if there's the 2nd @ because
+ // the backend user search can't deal with it.
+ // Reference semantics make it so that we get the updated data after
+ // the await.
+ if (!noPrefix.includes('@')) {
+ await debounceUserSearch(noPrefix)
+ }
+
+ const newSuggestions = state.users.users.filter(
+ user =>
+ user.screen_name.toLowerCase().startsWith(noPrefix) ||
+ user.name.toLowerCase().startsWith(noPrefix)
+ ).slice(0, 20).sort((a, b) => {
+ let aScore = 0
+ let bScore = 0
+
+ // Matches on screen name (i.e. user@instance) makes a priority
+ aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
+ bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
+
+ // Matches on name takes second priority
+ aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
+ bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
+
+ const diff = (bScore - aScore) * 10
+
+ // Then sort alphabetically
+ const nameAlphabetically = a.name > b.name ? 1 : -1
+ const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
+
+ return diff + nameAlphabetically + screenNameAlphabetically
+ /* eslint-disable camelcase */
+ }).map(({ screen_name, name, profile_image_url_original }) => ({
+ displayText: screen_name,
+ detailText: name,
+ imageUrl: profile_image_url_original,
+ replacement: '@' + screen_name + ' '
+ }))
+ /* eslint-enable camelcase */
+
+ suggestions = newSuggestions || []
+ return suggestions
}
- return newUsers
- /* eslint-enable camelcase */
}
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 2f14b5b2..51d50359 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -6,7 +6,7 @@
:users="accountsForEmoji[reaction.name]"
>
{{ exportLabel }}
{{ importLabel }}
diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue
index ecd71bf1..d6a03088 100644
--- a/src/components/exporter/exporter.vue
+++ b/src/components/exporter/exporter.vue
@@ -11,7 +11,7 @@
{{ exportButtonLabel }}
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index a33f6e87..e687d487 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -2,8 +2,9 @@