Skip to content

Commit

Permalink
Merge branch 'master' into feature/chelonia-in-service-worker
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Oct 15, 2024
2 parents e5f8ed2 + 01e9169 commit 60603b0
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 16 deletions.
3 changes: 2 additions & 1 deletion frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,5 +673,6 @@ export default (sbp('sbp/selectors/register', {
return sbp('okTurtles.eventQueue/queueEvent', 'ACTIONS-LOGIN', ['gi.actions/identity/_private/logout', ...params])
},
...encryptedAction('gi.actions/identity/saveFileDeleteToken', L('Failed to save delete tokens for the attachments.')),
...encryptedAction('gi.actions/identity/removeFileDeleteToken', L('Failed to remove delete tokens for the attachments.'))
...encryptedAction('gi.actions/identity/removeFileDeleteToken', L('Failed to remove delete tokens for the attachments.')),
...encryptedAction('gi.actions/identity/setGroupAttributes', L('Failed to set group attributes.'))
}): string[])
9 changes: 8 additions & 1 deletion frontend/controller/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ Vue.use(Router)
*/
const homeGuard = {
guard: (to, from) => !!store.state.currentGroupId,
redirect: (to, from) => ({ path: store.getters.ourProfileActive ? '/dashboard' : '/pending-approval' })
redirect: (to, from) => ({
path:
// If we haven't accepted the invite OR we haven't clicked 'Awesome' on
// the welcome screen, redirect to the '/pending-approval' page
store.getters.seenWelcomeScreen
? '/dashboard'
: '/pending-approval'
})
}

const loginGuard = {
Expand Down
31 changes: 28 additions & 3 deletions frontend/model/contracts/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { L } from '@common/common.js'
import sbp from '@sbp/sbp'
import { arrayOf, boolean, object, objectMaybeOf, objectOf, optional, string, stringMax, unionOf } from '~/frontend/model/contracts/misc/flowTyper.js'
import { arrayOf, boolean, object, objectMaybeOf, objectOf, optional, string, stringMax, unionOf, validatorFrom } from '~/frontend/model/contracts/misc/flowTyper.js'
import { LEFT_GROUP } from '~/frontend/utils/events.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { findForeignKeysByContractID, findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
Expand Down Expand Up @@ -283,8 +283,13 @@ sbp('chelonia/defineContract', {
throw new Error(`Cannot leave group ${groupContractID} because the reference hash does not match the latest`)
}

state.groups[groupContractID].hasLeft = true
delete state.groups[groupContractID].inviteSecret
// We only keep `hash` and `hasLeft` in the list of groups, as this
// is the only information we need for groups we're not part of.
// This has the advantage that we don't need to explicitly delete
// every new attribute that we may add in the future, but has the
// downside that, if we were to add a new attribute that's needed after
// having left, then it'd need to be added here.
state.groups[groupContractID] = { hash: reference, hasLeft: true }
},
sideEffect ({ data, contractID }) {
sbp('gi.contracts/identity/referenceTally', contractID, data.groupContractID, 'release')
Expand Down Expand Up @@ -359,6 +364,26 @@ sbp('chelonia/defineContract', {
delete state.fileDeleteTokens[manifestCid]
}
}
},
'gi.contracts/identity/setGroupAttributes': {
validate: objectOf({
groupContractID: string,
attributes: objectMaybeOf({
seenWelcomeScreen: validatorFrom((v) => v === true)
})
}),
process ({ data }, { state }) {
const { groupContractID, attributes } = data
if (!has(state.groups, groupContractID) || state.groups[groupContractID].hasLeft) {
throw new Error('Can\'t set attributes of groups you\'re not a member of')
}
if (attributes.seenWelcomeScreen) {
if (state.groups[groupContractID].seenWelcomeScreen) {
throw new Error('seenWelcomeScreen already set')
}
state.groups[groupContractID].seenWelcomeScreen = attributes.seenWelcomeScreen
}
}
}
},
methods: {
Expand Down
14 changes: 12 additions & 2 deletions frontend/views/components/GroupWelcome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
</template>

<script>
import { mapGetters } from 'vuex'
import sbp from '@sbp/sbp'
import { mapGetters, mapState } from 'vuex'
import Avatar from '@components/Avatar.vue'
import ConfettiAnimation from '@components/confetti-animation/ConfettiAnimation.vue'
Expand All @@ -41,7 +42,8 @@ export default ({
$v: { type: Object }
},
computed: {
...mapGetters(['groupSettings'])
...mapState(['currentGroupId']),
...mapGetters(['groupSettings', 'ourIdentityContractId'])
},
data () {
return {
Expand All @@ -52,6 +54,14 @@ export default ({
toDashboard () {
if (this.isButtonClicked) return
this.isButtonClicked = true
const groupContractID = this.currentGroupId
sbp('gi.actions/identity/setGroupAttributes', {
contractID: this.ourIdentityContractId,
data: {
groupContractID,
attributes: { seenWelcomeScreen: true }
}
}).catch(e => console.warn('[GroupWelcome.vue] Error setting seenWelcomeScreen attribute', groupContractID, e))
this.$router.push({ path: '/dashboard' })
}
}
Expand Down
13 changes: 13 additions & 0 deletions frontend/views/containers/chatroom/SendArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
@keydown.ctrl='isNextLine'
@keydown='handleKeydown'
@keyup='handleKeyup'
@paste='handlePaste'
v-bind='$attrs'
)

Expand Down Expand Up @@ -553,6 +554,18 @@ export default ({
this.updateMentionKeyword()
}
},
handlePaste (e) {
// fix for the edge-case related to 'paste' action when nothing has been typed
// (reference: https://github.com/okTurtles/group-income/issues/2369)
const currVal = this.$refs.textarea.value
if (!currVal) {
e.preventDefault()
const pastedText = e.clipboardData.getData('text')
this.$refs.textarea.value = pastedText
this.updateTextArea()
}
},
addSelectedMention (index) {
const curValue = this.$refs.textarea.value
const curPosition = this.$refs.textarea.selectionStart
Expand Down
15 changes: 9 additions & 6 deletions frontend/views/pages/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export default ({
},
computed: {
...mapGetters([
'ourProfileActive'
'currentIdentityState',
'seenWelcomeScreen'
]),
...mapState([
'currentGroupId'
Expand All @@ -103,11 +104,11 @@ export default ({
data () {
return {
ephemeral: {
ourProfileActive: false,
seenWelcomeScreen: false,
listener: ({ contractID }) => {
if (contractID !== this.currentGroupId) return
// For first time joins, force redirect to /pending-approval
this.ephemeral.ourProfileActive = false
this.ephemeral.seenWelcomeScreen = false
},
showPwaPromo: false
}
Expand All @@ -118,7 +119,7 @@ export default ({
this.checkPwaInstallability()
},
mounted () {
this.ephemeral.ourProfileActive = this.ourProfileActive
this.ephemeral.seenWelcomeScreen = this.seenWelcomeScreen
if (this.isLoggedIn && this.currentGroupId) {
this.navigateToGroupPage()
} else if (this.$route.query.next) {
Expand All @@ -140,7 +141,7 @@ export default ({
// In this particular condition, the app needs to immediately redirect user to '$route.query.next'
// so that the user stays in the same page after the browser refresh.
// (Related GH issue: https://github.com/okTurtles/group-income/issues/1830)
const path = this.$route.query.next ?? (this.ephemeral.ourProfileActive ? '/dashboard' : '/pending-approval')
const path = this.$route.query.next ?? (this.ephemeral.seenWelcomeScreen ? '/dashboard' : '/pending-approval')
this.$router.push({ path }).catch(e => ignoreWhenNavigationCancelled(e, path))
},
checkPwaInstallability () {
Expand All @@ -155,7 +156,9 @@ export default ({
},
watch: {
currentGroupId (to) {
this.ephemeral.ourProfileActive = this.ourProfileActive
// Redirect to `/pending-approval` if our profile isn't active or the
// welcome screen hasn't been approved
this.ephemeral.seenWelcomeScreen = this.seenWelcomeScreen
if (to && this.isLoggedIn) {
this.navigateToGroupPage()
}
Expand Down
19 changes: 16 additions & 3 deletions test/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import { CONTRACTS_MODIFIED_READY, EVENT_HANDLED_READY, EVENT_PUBLISHED, EVENT_P
const API_URL = Cypress.config('baseUrl')

// util funcs
const setGroupSeenWelcomeScreen = (sbp) => {
const state = sbp('state/vuex/state')
return sbp('gi.actions/identity/setGroupAttributes', {
contractID: state.loggedIn.identityContractID,
data: {
groupContractID: state.currentGroupId,
attributes: { seenWelcomeScreen: true }
}
})
}

const randomFromArray = arr => arr[Math.floor(Math.random() * arr.length)] // importing giLodash.js fails for some reason.
const getParamsFromInvitationLink = invitationLink => {
const params = new URLSearchParams(new URL(invitationLink).hash.slice(1))
Expand Down Expand Up @@ -231,7 +242,7 @@ Cypress.Commands.add('giLogin', (username, {
if (firstLoginAfterJoinGroup) {
const router = sbp('controller/router')
if (router.history.current.path === '/dashboard') return
return router.push({ path: '/dashboard' }) // .catch(() => {})
return setGroupSeenWelcomeScreen(sbp).then(() => router.push({ path: '/dashboard' })) // .catch(() => {})
}
})
})
Expand Down Expand Up @@ -331,7 +342,7 @@ Cypress.Commands.add('giCreateGroup', (name, {

const timeoutId = setTimeout(() => {
reject(new Error('[cypress] Timed out waiting for JOINED_GROUP event and active profile status'))
}, 5000)
}, 15000)

const cID = await sbp('gi.app/group/createAndSwitch', {
data: {
Expand All @@ -347,7 +358,7 @@ Cypress.Commands.add('giCreateGroup', (name, {
}).then(() => {
const router = sbp('controller/router')
if (router.history.current.path === '/dashboard') return
return router.push({ path: '/dashboard' })
return setGroupSeenWelcomeScreen(sbp).then(() => router.push({ path: '/dashboard' }))
})
})
cy.url().should('eq', `${API_URL}/app/dashboard`)
Expand Down Expand Up @@ -801,6 +812,8 @@ Cypress.Commands.add('giWaitUntilMessagesLoaded', (isGroupChannel = true) => {
})

Cypress.Commands.add('giSendMessage', (sender, message) => {
// The following is to ensure the chatroom has finished loading (no spinner)
cy.giWaitUntilMessagesLoaded(false)
cy.getByDT('messageInputWrapper').within(() => {
cy.get('textarea').type(`{selectall}{del}${message}{enter}`, { force: true })
cy.get('textarea').should('be.empty')
Expand Down

0 comments on commit 60603b0

Please sign in to comment.