Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Corrideat/task/#2341 anyone can join invite issue #2366

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
96e6592
increase the life of anyone-invite-link
SebinSong Sep 10, 2024
abe1666
update readableExpiryInfo method (incomplete)
SebinSong Sep 11, 2024
655bbfc
some refactoring in GroupMembers.vue and ProposalTemplate.vue
SebinSong Sep 11, 2024
c1f86a4
add generateInviteImmediately() method / make sure the invite works
SebinSong Sep 12, 2024
4c72344
Merge remote-tracking branch 'origin/master' into sebin/task/#2341-an…
SebinSong Sep 13, 2024
f638fb7
Merge remote-tracking branch 'origin/master' into sebin/task/#2341-an…
SebinSong Sep 15, 2024
2621b53
fix the enter issue / update submit-btn text
SebinSong Sep 15, 2024
f9ec6c5
revert the invite life
SebinSong Sep 15, 2024
95f1edc
update readable invite-life logic
SebinSong Sep 15, 2024
43e557e
update the period display expression again
SebinSong Sep 16, 2024
8a437e4
Merge remote-tracking branch 'origin/master' into sebin/task/#2341-an…
SebinSong Sep 18, 2024
c80ddeb
update comment
SebinSong Sep 18, 2024
709daac
Merge remote-tracking branch 'origin/master' into sebin/task/#2341-an…
SebinSong Sep 23, 2024
b31dd3e
implement gi.app/group/fixAnyoneCanJoinLink and use it
SebinSong Sep 24, 2024
eed98aa
Invite expires. TODO: Update exisiting invites
corrideat Sep 30, 2024
1459e8a
Recreate anyone can join
corrideat Oct 2, 2024
bd56e10
Fields for readable date
corrideat Oct 4, 2024
ba54c9b
Merge branch 'master' into corrideat/task/#2341-anyone-can-join-invit…
corrideat Oct 4, 2024
c93c63b
Fix types
corrideat Oct 4, 2024
f6b9f75
Merge branch 'master' into corrideat/task/#2341-anyone-can-join-invit…
corrideat Oct 5, 2024
feb3efd
Post upgrade verification for upgrading invites
corrideat Oct 5, 2024
81cc322
Use MAX_GROUP_MEMBER_COUNT instead of 150
corrideat Oct 6, 2024
713f8e3
Feedback
corrideat Oct 9, 2024
330624d
postUpgradeVerification improvements
corrideat Oct 10, 2024
1011aac
Filter for valid invites
corrideat Oct 10, 2024
74b69d1
Revert type changes
corrideat Oct 11, 2024
ca90a19
Feedback
corrideat Oct 12, 2024
d255eb4
Skip anyone expiration test if it doesnt expire
corrideat Oct 12, 2024
27ed1bd
Remove renundant check
corrideat Oct 13, 2024
d0ec792
Feedback
corrideat Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const {
MAX_EVENTS_AFTER = '',
NODE_ENV = 'development',
EXPOSE_SBP = '',
ENABLE_UNSAFE_NULL_CRYPTO = 'false'
ENABLE_UNSAFE_NULL_CRYPTO = 'false',
UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS = 'false'
} = process.env

if (!['development', 'production'].includes(NODE_ENV)) {
Expand Down Expand Up @@ -223,7 +224,8 @@ module.exports = (grunt) => {
'process.env.MAX_EVENTS_AFTER': `'${MAX_EVENTS_AFTER}'`,
'process.env.NODE_ENV': `'${NODE_ENV}'`,
'process.env.EXPOSE_SBP': `'${EXPOSE_SBP}'`,
'process.env.ENABLE_UNSAFE_NULL_CRYPTO': `'${ENABLE_UNSAFE_NULL_CRYPTO}'`
'process.env.ENABLE_UNSAFE_NULL_CRYPTO': `'${ENABLE_UNSAFE_NULL_CRYPTO}'`,
'process.env.UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS': UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent handling of this environment variable, could you please wrap it the same way as the others?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this is a boolean, not a string

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is ENABLE_UNSAFE_NULL_CRYPTO, I don't think it matters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does matter (although I can see the argument for consistency). See:

if (process.env.ENABLE_UNSAFE_NULL_CRYPTO === 'true' && ...

If you make it a string, then it's a string. If you don't quote it, it's whatever the type of that expression is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Well, can we make it consistent anyway, for consistency's sake?

},
external: ['crypto', '*.eot', '*.ttf', '*.woff', '*.woff2'],
format: 'esm',
Expand Down
77 changes: 74 additions & 3 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import { GIErrorUIRuntimeError, L, LError, LTags } from '@common/common.js'
import {
CHATROOM_PRIVACY_LEVEL,
INVITE_EXPIRES_IN_DAYS,
INVITE_INITIAL_CREATOR,
INVITE_EXPIRES_IN_DAYS,
MAX_GROUP_MEMBER_COUNT,
MESSAGE_TYPES,
PROFILE_STATUS,
PROPOSAL_GENERIC,
Expand All @@ -19,6 +20,7 @@ import {
STATUS_EXPIRED,
STATUS_CANCELLED
} from '@model/contracts/shared/constants.js'
import { doesGroupAnyoneCanJoinNeedUpdating } from '@model/contracts/shared/functions.js'
import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js'
import { DAYS_MILLIS, addTimeToDate, dateToPeriodStamp } from '@model/contracts/shared/time.js'
import proposals, { oneVoteToPass, oneVoteToFail } from '@model/contracts/shared/voting/proposals.js'
Expand All @@ -38,6 +40,7 @@ import { Secret } from '~/shared/domains/chelonia/Secret.js'
import type { ChelKeyRequestParams } from '~/shared/domains/chelonia/chelonia.js'
import { encryptedOutgoingData, encryptedOutgoingDataWithRawKey } from '~/shared/domains/chelonia/encryptedData.js'
import { CONTRACT_HAS_RECEIVED_KEYS, EVENT_HANDLED } from '~/shared/domains/chelonia/events.js'
import { findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
// Using relative path to crypto.js instead of ~-path to workaround some esbuild bug
import type { Key } from '../../../shared/domains/chelonia/crypto.js'
import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keyId, keygen, serializeKey } from '../../../shared/domains/chelonia/crypto.js'
Expand Down Expand Up @@ -180,8 +183,11 @@ export default (sbp('sbp/selectors/register', {
ringLevel: Number.MAX_SAFE_INTEGER,
permissions: [GIMessage.OP_KEY_REQUEST],
meta: {
quantity: 60,
expires: Date.now() + DAYS_MILLIS * INVITE_EXPIRES_IN_DAYS.ON_BOARDING,
quantity: MAX_GROUP_MEMBER_COUNT,
...(INVITE_EXPIRES_IN_DAYS.ON_BOARDING && {
expires:
await sbp('chelonia/time') * 1000 + DAYS_MILLIS * INVITE_EXPIRES_IN_DAYS.ON_BOARDING
}),
private: {
content: inviteKeyS
}
Expand Down Expand Up @@ -907,6 +913,70 @@ export default (sbp('sbp/selectors/register', {
sbp('okTurtles.events/emit', REPLACE_MODAL, 'IncomeDetails')
}
},
'gi.actions/group/fixAnyoneCanJoinLink': function ({ contractID }) {
// Queue ensures that the update happens as atomically as possible
return sbp('chelonia/queueInvocation', `${contractID}-FIX-ANYONE-CAN-JOIN`, async () => {
const now = await sbp('chelonia/time') * 1000
const state = await sbp('chelonia/contract/wait', contractID).then(() => sbp('chelonia/contract/state', contractID))

const quantity = doesGroupAnyoneCanJoinNeedUpdating(state)
if (!quantity) {
if (quantity === false) {
console.warn('[gi.actions/group/fixAnyoneCanJoinLink] Group has already been updated', contractID, MAX_GROUP_MEMBER_COUNT)
} else {
console.warn('[gi.actions/group/fixAnyoneCanJoinLink] Already used MAX_GROUP_MEMBER_COUNT invites for group', contractID, MAX_GROUP_MEMBER_COUNT)
}
return
}

const CEKid = findKeyIdByName(state, 'cek')
const CSKid = findKeyIdByName(state, 'csk')

if (!CEKid || !CSKid) {
throw new Error('Contract is missing a CEK or CSK')
}

const inviteKey = keygen(EDWARDS25519SHA512BATCH)
const inviteKeyId = keyId(inviteKey)
const inviteKeyP = serializeKey(inviteKey, false)
const inviteKeyS = encryptedOutgoingData(state, CEKid, serializeKey(inviteKey, true))

const ik = {
id: inviteKeyId,
name: '#inviteKey-' + inviteKeyId,
purpose: ['sig'],
ringLevel: Number.MAX_SAFE_INTEGER,
permissions: [GIMessage.OP_KEY_REQUEST],
meta: {
quantity,
...(INVITE_EXPIRES_IN_DAYS.ON_BOARDING && {
expires:
now + DAYS_MILLIS * INVITE_EXPIRES_IN_DAYS.ON_BOARDING
}),
private: {
content: inviteKeyS
}
},
data: inviteKeyP
}

// Replace all existing anyone-can-join invite links with the new one
const activeInvites = Object.keys(state.invites)
.filter(invite => state.invites[invite].creatorID === INVITE_INITIAL_CREATOR && !(state._vm.invites[invite].expires >= now))

await sbp('chelonia/out/keyAdd', {
contractName: 'gi.contracts/group',
contractID,
data: [ik],
signingKeyId: CSKid
})

await sbp('gi.actions/group/invite', { contractID, data: { inviteKeyId, creatorID: INVITE_INITIAL_CREATOR } })

// Revoke at the end
await Promise.all(activeInvites.map(inviteKeyId => sbp('gi.actions/group/inviteRevoke', { contractID, data: { inviteKeyId } })))
})
},
...encryptedAction('gi.actions/group/leaveChatRoom', L('Failed to leave chat channel.'), async (sendMessage, params) => {
const state = await sbp('chelonia/contract/state', params.contractID)
const memberID = params.data.memberID || sbp('chelonia/rootState').loggedIn.identityContractID
Expand Down Expand Up @@ -1041,6 +1111,7 @@ export default (sbp('sbp/selectors/register', {
...encryptedAction('gi.actions/group/updateSettings', L('Failed to update group settings.')),
...encryptedAction('gi.actions/group/updateAllVotingRules', (params, e) => L('Failed to update voting rules. {codeError}', { codeError: e.message })),
...encryptedAction('gi.actions/group/updateDistributionDate', L('Failed to update group distribution date.')),
...encryptedAction('gi.actions/group/updateGroupInviteExpiry', L('Failed to update group invite expiry.')),
...encryptedAction('gi.actions/group/upgradeFrom1.0.7', L('Failed to upgrade from version 1.0.7')),
...((process.env.NODE_ENV === 'development' || process.env.CI) && {
...encryptedAction('gi.actions/group/forceDistributionDate', L('Failed to force distribution date.'))
Expand Down
2 changes: 1 addition & 1 deletion frontend/model/contracts/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const MESSAGE_TYPES = {
}

export const INVITE_EXPIRES_IN_DAYS = {
ON_BOARDING: 30,
ON_BOARDING: null, // No expiration
PROPOSAL: 7
}

Expand Down
37 changes: 37 additions & 0 deletions frontend/model/contracts/shared/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import sbp from '@sbp/sbp'
import { L } from '@common/common.js'
import {
INVITE_INITIAL_CREATOR,
MAX_GROUP_MEMBER_COUNT,
MESSAGE_TYPES,
POLL_STATUS,
PROPOSAL_GROUP_SETTING_CHANGE,
Expand Down Expand Up @@ -311,3 +313,38 @@ export const referenceTally = (selector: string): Object => {
}
}
}

export const doesGroupAnyoneCanJoinNeedUpdating = (state: Object): number | false => {
const hasBeenUpdated = Object.keys(state.invites).some(inviteId => {
return (
// See if there's an 'anyone can join' link
state.invites[inviteId].creatorID === INVITE_INITIAL_CREATOR &&
// that doesn't expire
state._vm.invites[inviteId].expires == null
)
})
// If non-expiring 'anyone can join' links are found, the contract
// doesn't need to be updated
if (hasBeenUpdated) return false

// Add up all all used 'anyone can join' invite links
// Then, we take the difference and, if the number is less than
// MAX_GROUP_MEMBER_COUNT, we need to create a new invite.
const usedInvites = Object.keys(state.invites)
// First, we only want 'anyone can join invites'
.filter(invite => state.invites[invite].creatorID === INVITE_INITIAL_CREATOR)
// The reduce function adds the number of 'used' invites in an invite
// link across all existing (expired or not) invites
.reduce((acc, cv) => acc +
(
// For this, we take the difference between the `initialQuantity`
// and the 'available' invites (`quantity`, which represents how
// many invites are available, if the invite was still valid)
(state._vm.invites[cv].initialQuantity - state._vm.invites[cv].quantity) || 0
), 0
)

const quantity = Math.max(MAX_GROUP_MEMBER_COUNT - usedInvites, 0)

return quantity
}
8 changes: 7 additions & 1 deletion frontend/model/contracts/shared/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const MINS_MILLIS = 60000
export const HOURS_MILLIS = 60 * MINS_MILLIS
export const DAYS_MILLIS = 24 * HOURS_MILLIS
export const MONTHS_MILLIS = 30 * DAYS_MILLIS
export const YEARS_MILLIS = 365 * DAYS_MILLIS

export const plusOnePeriodLength = (timestamp: string, periodLength: number): string => (
dateToPeriodStamp(addTimeToDate(timestamp, periodLength))
Expand Down Expand Up @@ -184,7 +185,12 @@ export function humanDate (
: ((navigator.languages: any): string[]) ?? navigator.language ?? fallback
// NOTE: `.toLocaleDateString()` automatically takes local timezone differences into account.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString
return new Date(date).toLocaleDateString(locale, options)
const dateObj = new Date(date)

// Avoid returning 'Invalid Date'
if (!isNaN(dateObj.valueOf())) return dateObj.toLocaleDateString(locale, options)
corrideat marked this conversation as resolved.
Show resolved Hide resolved

return ''
}

export function isPeriodStamp (arg: string): boolean {
Expand Down
104 changes: 96 additions & 8 deletions frontend/model/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import sbp from '@sbp/sbp'
import { L } from '@common/common.js'
import { EVENT_HANDLED, CONTRACT_REGISTERED } from '~/shared/domains/chelonia/events.js'
import { doesGroupAnyoneCanJoinNeedUpdating } from '@model/contracts/shared/functions.js'
import { INVITE_STATUS } from '~/shared/domains/chelonia/constants.js'
import { LOGOUT } from '~/frontend/utils/events.js'
import Vue from 'vue'
import Vuex from 'vuex'
Expand All @@ -22,6 +24,57 @@ import identityGetters from './contracts/shared/getters/identity.js'
import notificationModule from '~/frontend/model/notifications/vuexModule.js'
import settingsModule from '~/frontend/model/settings/vuexModule.js'
import chatroomModule from '~/frontend/model/chatroom/vuexModule.js'
import { CHELONIA_RESET, CONTRACTS_MODIFIED } from '../../shared/domains/chelonia/events.js'

// Wrapper function for performing contract upgrades and migrations
// TODO: Consider moving this function into a different file
const contractUpdate = (initialState: Object, updateFn: (state: Object, contractIDHints: ?string[]) => any, contractType: ?string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please move contractType to be the 2nd or 1st parameter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't work that well because it's meant to be an optional parameter, as you may want to update contracts regardless of type.

// Wrapper for the update function. This performs a common check, namely that
// the contract is of a certain type, which helps return early
const wrappedUpdateFn = contractType
// The following disable is because eslint gets confused with 'Object'
// eslint-disable-next-line no-use-before-define
? (state: Object, contractIDHints: ?string[]) => {
if (Array.isArray(contractIDHints)) {
if (!contractIDHints.reduce((acc, contractID) => {
return (acc || state.contracts[contractID]?.type === contractType)
}, false)) return
Copy link
Member

@taoeffect taoeffect Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code seems to me, unnecessarily confusing. Could you please replace this with something simpler, e.g.:

if (!contractIDHints.some(contractID => state.contracts[contractID]?.type === contractType)) {
  return
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

}
updateFn(state, contractIDHints)
}
: updateFn

const resetHandler = () => {
sbp('okTurtles.events/off', CONTRACTS_MODIFIED, modifiedHandler)
}
Comment on lines +47 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is 1 line and it is only used once with a /once listener, it can be inlined as an anonymous function below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that you preferred named functions over inlined ones (and it's also more consistent in this case)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do, usually, it especially helps when there's a large or confusing chunk of logic to simplify, or to DRY things. In this case it's simple enough to inline and there's no DRY violation. However, it's not a big to leave as-is too of you prefer.

// This function is called when the set of subscribed contracts is modified
const modifiedHandler = (_, { added }) => {
// Wait for the added contracts to be ready, then call the update function
sbp('chelonia/contract/wait', added).then(() => {
const state = sbp('state/vuex/state')
wrappedUpdateFn(state, added)
})
}

// Add event listeners for `CONTRACTS_MODIFIED` and `CHELONIA_RESET` events
// `CONTRACTS_MODIFIED` is the important event. This is what allows updating
// contracts that are newly synced (for example, when logging in for the
// first time or joining an existing group or chatroom)
sbp('okTurtles.events/on', CONTRACTS_MODIFIED, modifiedHandler)
// Receiving `CHELONIA_RESET` means that a new session has started. To prevent
// memory leaks and duplicate handlers, this event will remove the
// `CONTRACTS_MODIFIED` handler.
sbp('okTurtles.events/once', CHELONIA_RESET, resetHandler)

// Call the update function in the next tick
// We want this (in addition to `CONTRACTS_MODIFIED`) because this way we
// can update contracts that already exist, e.g., upon login with a saved
// state
corrideat marked this conversation as resolved.
Show resolved Hide resolved
const existingContracts = Object.keys(initialState.contracts)
setTimeout(() => {
wrappedUpdateFn(initialState, existingContracts)
}, 0)
}

Vue.use(Vuex)

Expand Down Expand Up @@ -51,6 +104,21 @@ const checkedUsername = (state: Object, username: string, userID: string) => {
}
}

// Find the 'anyone can join' invite ID. Since there could be multiple, and some
// of those could have exipred, we need a for loop
const anyoneCanJoinInviteId = (invites: Object, getters: Object): ?string =>
Object.keys(invites).find(invite =>
// First, we want 'anyone can join' invites
invites[invite].creatorID === INVITE_INITIAL_CREATOR &&
// and that haven't been revoked
getters.currentGroupState._vm.invites[invite].status === INVITE_STATUS.VALID &&
// and that haven't expired (using negative logic because expires could be
// undefined for non expiring-invites)
!(getters.currentGroupState._vm.invites[invite].expires >= Date.now()) &&
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
// and that that haven't been entirely used up
!(getters.currentGroupState._vm.invites[invite].quantity <= 0)
)

const reactiveDate = Vue.observable({ date: new Date() })
setInterval(function () {
// We want the getters to recalculate all of the payments within 1 minute of us entering a new period.
Expand Down Expand Up @@ -90,7 +158,7 @@ sbp('sbp/selectors/register', {
// $FlowFixMe[incompatible-call]
Vue.set(state, 'reverseNamespaceLookups', Object.fromEntries(Object.entries(state.namespaceLookups).map(([k, v]: [string, string]) => [v, k])))
}
(() => {
contractUpdate(state, (state: Object, contractIDHints: ?string[]) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Claude if it could make the code from line 170 to 182 more readable, and here's what it suggested:

Object.entries(state[ourIdentityContractId].groups)
  .filter(([groupID, { hasLeft }]) => {
    return !hasLeft && 
           state[groupID]?.chatRooms && 
           (!Array.isArray(contractIDHints) || contractIDHints.includes(groupID))
  })
  .map(([groupID]) => {
    // $FlowFixMe[incompatible-use]
    const chatRooms = state[groupID].chatRooms
    const needsUpgrade = Object.values(chatRooms)
      .flatMap(({ members }) => Object.values(members))
      .some(member => 
        member.status === PROFILE_STATUS.ACTIVE && member.joinedHeight == null
      )

    return needsUpgrade ? groupID : null
  })
  .filter(Boolean)

If this accomplishes the same thing, do you think it's safe to replace it with this?

Copy link
Member Author

@corrideat corrideat Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, I'd need to see if they do the same thing or not. So far, it seems like it might do the same thing.

// Upgrade from version 1.0.7 to a newer version
// The new group contract introduces a breaking change: the
// `state[groupID].chatRooms[chatRoomID].members[memberID].joinedHeight`
Expand All @@ -101,6 +169,7 @@ sbp('sbp/selectors/register', {
if (!ourIdentityContractId || !state[ourIdentityContractId]?.groups) return
Object.entries(state[ourIdentityContractId].groups).map(([groupID, { hasLeft }]: [string, Object]) => {
if (hasLeft || !state[groupID]?.chatRooms) return undefined
if (Array.isArray(contractIDHints) && !contractIDHints.includes(groupID)) return undefined
Copy link
Member

@taoeffect taoeffect Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the point of the wrappedUpdateFn to handle this?

If this line is needed, can you add a 1-line comment above it explaining why wrappedUpdateFn is insufficient?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a comment. It's not sufficient because wrappedUpdateFn is filtering contracts by type, while this is filtering out contracts (groups) that haven't been recently added.

// $FlowFixMe[incompatible-use]
return Object.values((state[groupID].chatRooms: { [string]: Object })).flatMap(({ members }) => {
return Object.values(members)
Expand All @@ -117,13 +186,16 @@ sbp('sbp/selectors/register', {
console.error('[state/vuex/postUpgradeVerification] Error during gi.actions/group/upgradeFrom1.0.7', contractID, e)
})
})
})();
(() => {
}, 'gi.contracts/group')
contractUpdate(state, (contractIDHints: ?string[]) => {
// Upgrade from version 1.0.8 to a newer version
// The new chatroom contracts have an admin IDs list
// This code checks if the attribute is missing, and if so, issues the
// corresponing upgrade action.
const needsUpgrade = (chatroomID) => !Array.isArray(state[chatroomID]?.attributes?.adminIDs)
const needsUpgrade = (chatroomID) => {
if (Array.isArray(contractIDHints) && !contractIDHints.includes(chatroomID)) return false
return !Array.isArray(state[chatroomID]?.attributes?.adminIDs)
}

const upgradeAction = async (contractID: string, data?: Object) => {
try {
Expand Down Expand Up @@ -157,7 +229,23 @@ sbp('sbp/selectors/register', {
upgradeAction(contractID)
})
}
})()
}, 'gi.contracts/chatroom')
contractUpdate(state, (contractIDHints: ?string[]) => {
// Update expired invites
// If fewer than MAX_GROUP_MEMBER_COUNT 'anyone can join' have been used,
// create a new 'anyone can join' link up to MAX_GROUP_MEMBER_COUNT invites
corrideat marked this conversation as resolved.
Show resolved Hide resolved
const ourIdentityContractId = state.loggedIn?.identityContractID
if (!ourIdentityContractId || !state[ourIdentityContractId]?.groups) return
Object.entries(state[ourIdentityContractId].groups).map(([groupID, { hasLeft }]: [string, Object]) => {
const groupState = state[groupID]
if (hasLeft || !groupState?.invites) return undefined
if (Array.isArray(contractIDHints) && !contractIDHints.includes(groupID)) return undefined
const needsUpdate = !!doesGroupAnyoneCanJoinNeedUpdating(groupState)
return needsUpdate ? groupID : undefined
}).filter(Boolean).forEach((contractID) => {
sbp('gi.actions/group/fixAnyoneCanJoinLink', { contractID }).catch(e => console.error(`[state/vuex/postUpgradeVerification] Error during gi.actions/group/fixAnyoneCanJoinLink for ${contractID}:`, e))
})
}, 'gi.contracts/group')
},
'state/vuex/save': (encrypted: ?boolean, state: ?Object) => {
return sbp('okTurtles.eventQueue/queueEvent', 'state/vuex/save', async function () {
Expand Down Expand Up @@ -499,8 +587,8 @@ const getters = {
},
currentWelcomeInvite (state, getters) {
const invites = getters.currentGroupState.invites
const inviteId = Object.keys(invites).find(invite => invites[invite].creatorID === INVITE_INITIAL_CREATOR)
const expires = getters.currentGroupState._vm.authorizedKeys[inviteId].meta.expires
const inviteId = anyoneCanJoinInviteId(invites, getters)
const expires = getters.currentGroupState._vm.invites[inviteId].expires
return { inviteId, expires }
},
// list of group names and contractIDs
Expand Down Expand Up @@ -552,7 +640,7 @@ const getters = {
// $FlowFixMe[method-unbinding]
return [groupMembersPending, getters.groupProfiles].flatMap(Object.keys)
.filter(memberID => getters.groupProfiles[memberID] ||
getters.groupMembersPending[memberID].expires >= Date.now())
!(getters.groupMembersPending[memberID].expires < Date.now()))
.map(memberID => {
const { contractID, displayName, username } = getters.globalProfile(memberID) || groupMembersPending[memberID] || (getters.groupProfiles[memberID] ? { contractID: memberID } : {})
return {
Expand Down
Loading