Merge remote-tracking branch 'origin/develop' into navigation-update

* origin/develop:
  Update dependency opn to v5
  fix notices being under the navbar, also change offset to use variable
  fix modals not having proper z index
  reduce indexes to be below 9999 so that develop error messages appear above
  Do not allow to find by name in findUser()
  Use lookup endpoint to obtain users by nickname
  Use $ for id UserProfile routes
  Allow opening profile: multiChoiceProprties record, anonymous access
  Allow opening profile when clicking an avatar inside of user popover
This commit is contained in:
Henry Jameson 2022-08-23 21:52:17 +03:00
commit cd7380efe7
16 changed files with 139 additions and 58 deletions

View File

@ -101,7 +101,7 @@
"mini-css-extract-plugin": "2.6.1", "mini-css-extract-plugin": "2.6.1",
"mocha": "10.0.0", "mocha": "10.0.0",
"nightwatch": "2.3.3", "nightwatch": "2.3.3",
"opn": "4.0.2", "opn": "5.5.0",
"ora": "0.4.1", "ora": "0.4.1",
"postcss": "8.4.16", "postcss": "8.4.16",
"postcss-loader": "7.0.1", "postcss-loader": "7.0.1",

View File

@ -5,12 +5,12 @@
--navbar-height: 3.5rem; --navbar-height: 3.5rem;
--post-line-height: 1.4; --post-line-height: 1.4;
// Z-Index stuff // Z-Index stuff
--ZI_media_modal: 90000; --ZI_media_modal: 9000;
--ZI_modals_popovers: 85000; --ZI_modals_popovers: 8500;
--ZI_modals: 80000; --ZI_modals: 8000;
--ZI_navbar_popovers: 75000; --ZI_navbar_popovers: 7500;
--ZI_navbar: 70000; --ZI_navbar: 7000;
--ZI_popovers: 60000; --ZI_popovers: 6000;
} }
html { html {
@ -158,6 +158,11 @@ nav {
grid-area: sidebar; grid-area: sidebar;
} }
#modal {
position: absolute;
z-index: var(--ZI_modals);
}
.column.-scrollable { .column.-scrollable {
top: var(--navbar-height); top: var(--navbar-height);
position: sticky; position: sticky;

View File

@ -62,7 +62,7 @@ export default (store) => {
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute
}, },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile }, { name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
@ -76,7 +76,8 @@ export default (store) => {
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { 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: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{ name: 'about', path: '/about', component: About }, { name: 'about', path: '/about', component: About },
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }, { name: 'user-profile', path: '/users/:name', component: UserProfile },
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
{ name: 'lists', path: '/lists', component: Lists }, { name: 'lists', path: '/lists', component: Lists },
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },

View File

@ -29,10 +29,10 @@
.global-notice-list { .global-notice-list {
position: fixed; position: fixed;
top: 50px; top: calc(var(--navbar-height) + 0.5em);
width: 100%; width: 100%;
pointer-events: none; pointer-events: none;
z-index: var(--ZI_popovers); z-index: var(--ZI_navbar_popovers);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@ -44,6 +44,11 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.third_column_mode_${mode}`) label: this.$t(`settings.third_column_mode_${mode}`)
})), })),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
loopSilentAvailable: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||

View File

@ -60,12 +60,14 @@
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting <ChoiceSetting
path="userPopoverZoom" id="userPopoverAvatarAction"
path="userPopoverAvatarAction"
:options="userPopoverAvatarActionOptions"
expert="1" expert="1"
> >
{{ $t('settings.user_popover_avatar_zoom') }} {{ $t('settings.user_popover_avatar_action') }}
</BooleanSetting> </ChoiceSetting>
</li> </li>
<li> <li>
<BooleanSetting <BooleanSetting

View File

@ -11,8 +11,8 @@ const UserPopover = {
Popover: defineAsyncComponent(() => import('../popover/popover.vue')) Popover: defineAsyncComponent(() => import('../popover/popover.vue'))
}, },
computed: { computed: {
userPopoverZoom () { userPopoverAvatarAction () {
return this.$store.getters.mergedConfig.userPopoverZoom return this.$store.getters.mergedConfig.userPopoverAvatarAction
}, },
userPopoverOverlay () { userPopoverOverlay () {
return this.$store.getters.mergedConfig.userPopoverOverlay return this.$store.getters.mergedConfig.userPopoverOverlay

View File

@ -14,7 +14,7 @@
class="user-popover" class="user-popover"
:user-id="userId" :user-id="userId"
:hide-bio="true" :hide-bio="true"
:avatar-action="userPopoverZoom ? 'zoom' : close" :avatar-action="userPopoverAvatarAction == 'close' ? close : userPopoverAvatarAction"
:on-close="close" :on-close="close"
/> />
</template> </template>

View File

@ -45,7 +45,7 @@ const UserProfile = {
}, },
created () { created () {
const routeParams = this.$route.params const routeParams = this.$route.params
this.load(routeParams.name || routeParams.id) this.load({ name: routeParams.name, id: routeParams.id })
this.tab = get(this.$route, 'query.tab', defaultTabKey) this.tab = get(this.$route, 'query.tab', defaultTabKey)
}, },
unmounted () { unmounted () {
@ -106,12 +106,17 @@ const UserProfile = {
this.userId = null this.userId = null
this.error = false this.error = false
const maybeId = userNameOrId.id
const maybeName = userNameOrId.name
// Check if user data is already loaded in store // Check if user data is already loaded in store
const user = this.$store.getters.findUser(userNameOrId) const user = maybeId ? this.$store.getters.findUser(maybeId) : this.$store.getters.findUserByName(maybeName)
if (user) { if (user) {
loadById(user.id) loadById(user.id)
} else { } else {
this.$store.dispatch('fetchUser', userNameOrId) (maybeId
? this.$store.dispatch('fetchUser', maybeId)
: this.$store.dispatch('fetchUserByName', maybeName))
.then(({ id }) => loadById(id)) .then(({ id }) => loadById(id))
.catch((reason) => { .catch((reason) => {
const errorMessage = get(reason, 'error.error') const errorMessage = get(reason, 'error.error')
@ -150,12 +155,12 @@ const UserProfile = {
watch: { watch: {
'$route.params.id': function (newVal) { '$route.params.id': function (newVal) {
if (newVal) { if (newVal) {
this.switchUser(newVal) this.switchUser({ id: newVal })
} }
}, },
'$route.params.name': function (newVal) { '$route.params.name': function (newVal) {
if (newVal) { if (newVal) {
this.switchUser(newVal) this.switchUser({ name: newVal })
} }
}, },
'$route.query': function (newVal) { '$route.query': function (newVal) {

View File

@ -584,7 +584,10 @@
"mention_link_show_avatar_quick": "Show user avatar next to mentions", "mention_link_show_avatar_quick": "Show user avatar next to mentions",
"mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)", "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
"mention_link_bolden_you": "Highlight mention of you when you are mentioned", "mention_link_bolden_you": "Highlight mention of you when you are mentioned",
"user_popover_avatar_zoom": "Clicking on user avatar in popover zooms it instead of closing the popover", "user_popover_avatar_action": "Popover avatar click action",
"user_popover_avatar_action_zoom": "Zoom the avatar",
"user_popover_avatar_action_close": "Close the popover",
"user_popover_avatar_action_open": "Open profile",
"user_popover_avatar_overlay": "Show user popover over user avatar", "user_popover_avatar_overlay": "Show user popover over user avatar",
"fun": "Fun", "fun": "Fun",
"greentext": "Meme arrows", "greentext": "Meme arrows",

View File

@ -17,7 +17,8 @@ export const multiChoiceProperties = [
'subjectLineBehavior', 'subjectLineBehavior',
'conversationDisplay', // tree | linear 'conversationDisplay', // tree | linear
'conversationOtherRepliesButton', // below | inside 'conversationOtherRepliesButton', // below | inside
'mentionLinkDisplay' // short | full_for_remote | full 'mentionLinkDisplay', // short | full_for_remote | full
'userPopoverAvatarAction' // close | zoom | open
] ]
export const defaultState = { export const defaultState = {
@ -82,7 +83,7 @@ export const defaultState = {
useContainFit: true, useContainFit: true,
disableStickyHeaders: false, disableStickyHeaders: false,
showScrollbars: false, showScrollbars: false,
userPopoverZoom: false, userPopoverAvatarAction: 'close',
userPopoverOverlay: true, userPopoverOverlay: true,
sidebarColumnWidth: '25rem', sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem', contentColumnWidth: '45rem',

View File

@ -16,9 +16,6 @@ export const mergeOrAdd = (arr, obj, item) => {
// This is a new item, prepare it // This is a new item, prepare it
arr.push(item) arr.push(item)
obj[item.id] = item obj[item.id] = item
if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name.toLowerCase()] = item
}
return { item, new: true } return { item, new: true }
} }
} }
@ -162,7 +159,11 @@ export const mutations = {
if (user.relationship) { if (user.relationship) {
state.relationships[user.relationship.id] = user.relationship state.relationships[user.relationship.id] = user.relationship
} }
mergeOrAdd(state.users, state.usersObject, user) const res = mergeOrAdd(state.users, state.usersObject, user)
const item = res.item
if (res.new && item.screen_name && !item.screen_name.includes('@')) {
state.usersByNameObject[item.screen_name.toLowerCase()] = item
}
}) })
}, },
updateUserRelationship (state, relationships) { updateUserRelationship (state, relationships) {
@ -242,12 +243,10 @@ export const mutations = {
export const getters = { export const getters = {
findUser: state => query => { findUser: state => query => {
const result = state.usersObject[query] return state.usersObject[query]
// In case it's a screen_name, we can try searching case-insensitive },
if (!result && typeof query === 'string') { findUserByName: state => query => {
return state.usersObject[query.toLowerCase()] return state.usersByNameObject[query.toLowerCase()]
}
return result
}, },
findUserByUrl: state => query => { findUserByUrl: state => query => {
return state.users return state.users
@ -266,6 +265,7 @@ export const defaultState = {
currentUser: false, currentUser: false,
users: [], users: [],
usersObject: {}, usersObject: {},
usersByNameObject: {},
signUpPending: false, signUpPending: false,
signUpErrors: [], signUpErrors: [],
relationships: {} relationships: {}
@ -288,6 +288,13 @@ const users = {
return user return user
}) })
}, },
fetchUserByName (store, name) {
return store.rootState.api.backendInteractor.fetchUserByName({ name })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship (store, id) { fetchUserRelationship (store, id) {
if (store.state.currentUser) { if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserRelationship({ id }) store.rootState.api.backendInteractor.fetchUserRelationship({ id })

View File

@ -50,6 +50,7 @@ const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup'
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
const MASTODON_USER_IN_LISTS = id => `/api/v1/accounts/${id}/lists` const MASTODON_USER_IN_LISTS = id => `/api/v1/accounts/${id}/lists`
@ -326,6 +327,25 @@ const fetchUser = ({ id, credentials }) => {
.then((data) => parseUser(data)) .then((data) => parseUser(data))
} }
const fetchUserByName = ({ name, credentials }) => {
return promisedRequest({
url: MASTODON_USER_LOOKUP_URL,
credentials,
params: { acct: name }
})
.then(data => data.id)
.catch(error => {
if (error && error.statusCode === 404) {
// Either the backend does not support lookup endpoint,
// or there is no user with such name. Fallback and treat name as id.
return name
} else {
throw error
}
})
.then(id => fetchUser({ id, credentials }))
}
const fetchUserRelationship = ({ id, credentials }) => { const fetchUserRelationship = ({ id, credentials }) => {
const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}` const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
@ -1489,6 +1509,7 @@ const apiService = {
blockUser, blockUser,
unblockUser, unblockUser,
fetchUser, fetchUser,
fetchUserByName,
fetchUserRelationship, fetchUserRelationship,
favorite, favorite,
unfavorite, unfavorite,

View File

@ -15,6 +15,7 @@ const actions = {
const testGetters = { const testGetters = {
findUser: state => getters.findUser(state.users), findUser: state => getters.findUser(state.users),
findUserByName: state => getters.findUserByName(state.users),
relationship: state => getters.relationship(state.users), relationship: state => getters.relationship(state.users),
mergedConfig: state => ({ mergedConfig: state => ({
colors: '', colors: '',
@ -95,6 +96,7 @@ const externalProfileStore = createStore({
credentials: '' credentials: ''
}, },
usersObject: { 100: extUser }, usersObject: { 100: extUser },
usersByNameObject: {},
users: [extUser], users: [extUser],
relationships: {} relationships: {}
} }
@ -163,7 +165,8 @@ const localProfileStore = createStore({
currentUser: { currentUser: {
credentials: '' credentials: ''
}, },
usersObject: { 100: localUser, testuser: localUser }, usersObject: { 100: localUser },
usersByNameObject: { testuser: localUser },
users: [localUser], users: [localUser],
relationships: {} relationships: {}
} }

View File

@ -57,24 +57,27 @@ describe('The users module', () => {
}) })
describe('findUser', () => { describe('findUser', () => {
it('returns user with matching screen_name', () => { it('does not return user with matching screen_name', () => {
const user = { screen_name: 'Guy', id: '1' } const user = { screen_name: 'Guy', id: '1' }
const state = { const state = {
usersObject: { usersObject: {
1: user, 1: user
},
usersByNameObject: {
guy: user guy: user
} }
} }
const name = 'Guy' const name = 'Guy'
const expected = { screen_name: 'Guy', id: '1' } expect(getters.findUser(state)(name)).to.eql(undefined)
expect(getters.findUser(state)(name)).to.eql(expected)
}) })
it('returns user with matching id', () => { it('returns user with matching id', () => {
const user = { screen_name: 'Guy', id: '1' } const user = { screen_name: 'Guy', id: '1' }
const state = { const state = {
usersObject: { usersObject: {
1: user, 1: user
},
usersByNameObject: {
guy: user guy: user
} }
} }
@ -83,4 +86,35 @@ describe('The users module', () => {
expect(getters.findUser(state)(id)).to.eql(expected) expect(getters.findUser(state)(id)).to.eql(expected)
}) })
}) })
describe('findUserByName', () => {
it('returns user with matching screen_name', () => {
const user = { screen_name: 'Guy', id: '1' }
const state = {
usersObject: {
1: user
},
usersByNameObject: {
guy: user
}
}
const name = 'Guy'
const expected = { screen_name: 'Guy', id: '1' }
expect(getters.findUserByName(state)(name)).to.eql(expected)
})
it('does not return user with matching id', () => {
const user = { screen_name: 'Guy', id: '1' }
const state = {
usersObject: {
1: user
},
usersByNameObject: {
guy: user
}
}
const id = '1'
expect(getters.findUserByName(state)(id)).to.eql(undefined)
})
})
}) })

View File

@ -5188,6 +5188,11 @@ is-weakref@^1.0.1:
dependencies: dependencies:
call-bind "^1.0.2" call-bind "^1.0.2"
is-wsl@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==
is-wsl@^2.2.0: is-wsl@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
@ -6551,13 +6556,12 @@ open@^8.4.0:
is-docker "^2.1.1" is-docker "^2.1.1"
is-wsl "^2.2.0" is-wsl "^2.2.0"
opn@4.0.2: opn@5.5.0:
version "4.0.2" version "5.5.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
integrity sha1-erwi5kTf9jsKltWrfyeQwPAavJU= integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
dependencies: dependencies:
object-assign "^4.0.1" is-wsl "^1.1.0"
pinkie-promise "^2.0.0"
optimist@^0.6.1: optimist@^0.6.1:
version "0.6.1" version "0.6.1"
@ -6835,16 +6839,6 @@ pify@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
pinkie-promise@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
dependencies:
pinkie "^2.0.0"
pinkie@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
pirates@^4.0.5: pirates@^4.0.5:
version "4.0.5" version "4.0.5"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"