diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..7a775184b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: Discuss Group Income + url: https://github.com/okTurtles/group-income/discussions + about: Use issues to report bugs or request features. Everything else goes here. + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/create-issue.md b/.github/ISSUE_TEMPLATE/create-issue.md new file mode 100644 index 0000000000..c6523735ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/create-issue.md @@ -0,0 +1,16 @@ +--- +name: Create Issue +about: Template for creating an issue, whether a bug report or a feature request. +title: '' +labels: '' +assignees: '' + +--- + +### Problem + +_Describe a clear problem here._ + +### Solution + +_Do your best to describe what the solution should look like._ diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 16449139f2..0000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,20 +0,0 @@ -### Instructions - -All issues must be in problem-solution format. - -- **Bad:** _"Problem: we need feature X. Solution: make it."_ -- **Better:** _"Problem: user cannot do common tasks A or B except by using a complex workaround. Solution: make feature X."_ - -Per [C4.1](https://rfc.zeromq.org/spec:42/C4/), "seek consensus on the accuracy of your observation, and the value of solving the problem. Do not submit feature requests, ideas, suggestions, or any solutions to problems that are not explicitly documented and provable." - -Before submitting, search for existing issues and read [`Troubleshooting.md`](docs/Troubleshooting.md). - -***! Delete this line and everything above before submitting !*** - -### Problem - -*Clearly describe the problem you observe, making sure to include any necessary debug output and/or screenshots.* - -### Solution - -*Describe, in whatever detail you feel comfortable, an acceptable solution as you envision it.* diff --git a/backend/zkppSalt.js b/backend/zkppSalt.js index 96f49770d8..0967156d93 100644 --- a/backend/zkppSalt.js +++ b/backend/zkppSalt.js @@ -105,7 +105,7 @@ const getZkppSaltRecord = async (contractID: string) => { !Array.isArray(recordObj) || (recordObj.length !== 3 && recordObj.length !== 4) || recordObj.slice(0, 3).some((r) => !r || typeof r !== 'string') || - (recordObj[3] !== null && typeof recordObj[3] !== 'string') + (recordObj[3] != null && typeof recordObj[3] !== 'string') ) { console.error('Error validating encrypted JSON object ' + recordId) return null diff --git a/frontend/controller/serviceworkers/sw-primary.js b/frontend/controller/serviceworkers/sw-primary.js index da37cc9977..f707346d12 100644 --- a/frontend/controller/serviceworkers/sw-primary.js +++ b/frontend/controller/serviceworkers/sw-primary.js @@ -215,7 +215,23 @@ sbp('sbp/selectors/register', { return computedGetters } - })() + })(), + // For compatibility for old contracts. + // TODO: This is to be deleted in a later release, once contracts are recreated. + 'state/vuex/commit': (key, data) => { + switch (key) { + case 'deleteChatRoomScrollPosition': + case 'setChatRoomScrollPosition': + return sbp('okTurtles.events/emit', NEW_CHATROOM_UNREAD_POSITION, data) + // These are handled by events + case 'removeNotification': + return + case 'setCurrentChatRoomId': + return + } + + throw new Error(`Invalid selector 'state/vuex/commit': Not allowed in SW. (key: ${key})`) + } }) const ourLocation = new URL(self.location) diff --git a/frontend/model/contracts/shared/constants.js b/frontend/model/contracts/shared/constants.js index 1e92cae0f5..0e7914c4e1 100644 --- a/frontend/model/contracts/shared/constants.js +++ b/frontend/model/contracts/shared/constants.js @@ -21,7 +21,7 @@ export const PROFILE_STATUS = { } export const GROUP_NAME_MAX_CHAR = 50 // https://github.com/okTurtles/group-income/issues/2196 export const GROUP_DESCRIPTION_MAX_CHAR = 500 -export const GROUP_PAYMENT_METHOD_MAX_CHAR = 250 +export const GROUP_PAYMENT_METHOD_MAX_CHAR = 1024 export const GROUP_NON_MONETARY_CONTRIBUTION_MAX_CHAR = 150 export const GROUP_CURRENCY_MAX_CHAR = 10 export const GROUP_MAX_PLEDGE_AMOUNT = 1000000000 diff --git a/frontend/model/database.js b/frontend/model/database.js index 8a97aa9c37..fe62f389dc 100644 --- a/frontend/model/database.js +++ b/frontend/model/database.js @@ -26,26 +26,43 @@ const localforage = { reject(new Error('Unsupported characters in name: -')) return } - const request = self.indexedDB.open(name + '--' + storeName) - // Create the object store if it doesn't exist - request.onupgradeneeded = (event) => { - const db = event.target.result - db.createObjectStore(storeName) + const openDB = (version?: number) => { + // By default `version` is the latest DB version. Initially, we + // try to open that, but in some cases (e.g., when manually + // deleting the DBs), the schema will be wrong and miss the object + // store. In these cases, we need to upgrade the DB by + // incrementing the version number to re-create the schema, which + // can only be done when the DB is being 'upgraded'. + const request = self.indexedDB.open(name + '--' + storeName, version) + + // Create the object store if it doesn't exist + request.onupgradeneeded = (event) => { + const db = event.target.result + db.createObjectStore(storeName) + } + + request.onsuccess = (event) => { + const db = event.target.result + if (!db.objectStoreNames.contains(storeName)) { + return openDB(db.version + 1) + } + + resolve(db) + } + + request.onerror = (error) => { + reject(error) + } + + // If this happens, closing all tabs and stopping the SW could + // help. + request.onblocked = (event) => { + reject(new Error('DB is blocked')) + } } - request.onsuccess = (event) => { - const db = event.target.result - resolve(db) - } - - request.onerror = (error) => { - reject(error) - } - - request.onblocked = (event) => { - reject(new Error('DB is blocked')) - } + openDB() }) } return promise diff --git a/frontend/model/notifications/mainPeriodicNotificationEntries.js b/frontend/model/notifications/mainPeriodicNotificationEntries.js index a6e61e1102..9f7af3f92e 100644 --- a/frontend/model/notifications/mainPeriodicNotificationEntries.js +++ b/frontend/model/notifications/mainPeriodicNotificationEntries.js @@ -38,7 +38,7 @@ const periodicNotificationEntries: { const comparison = comparePeriodStamps(nextPeriod, now) return ( - rootGetters.ourGroupProfileForGroup(rootState[gId]).incomeDetailsType === 'pledgeAmount' && + rootGetters.ourGroupProfileForGroup(rootState[gId])?.incomeDetailsType === 'pledgeAmount' && (comparison > 0 && comparison < DAYS_MILLIS * 7) && (rootGetters.ourPaymentsForGroup(rootState[gId])?.todo.length > 0) && !myNotificationHas(item => item.type === 'NEAR_DISTRIBUTION_END' && item.data.period === currentPeriod, gId) @@ -84,7 +84,7 @@ const periodicNotificationEntries: { this.nextDistributionPeriod = groupIds.map((gId) => { const profile = rootGetters.ourGroupProfileForGroup(rootState[gId]) - if (!profile.incomeDetailsType) { + if (!profile?.incomeDetailsType) { // if income-details are not set yet, ignore. return null } diff --git a/frontend/views/components/LinkToCopy.vue b/frontend/views/components/LinkToCopy.vue index 890538c6e4..dd651dc2a5 100755 --- a/frontend/views/components/LinkToCopy.vue +++ b/frontend/views/components/LinkToCopy.vue @@ -18,6 +18,7 @@ component.c-wrapper( tooltip.c-feedback( v-if='ephemeral.isTooltipActive' :isVisible='true' + :anchorToElement='true' direction='top' :text='L("Copied to clipboard!")' ) @@ -100,7 +101,7 @@ export default ({ } .c-feedback { - position: absolute; + position: absolute !important; left: 50%; transform: translateX(-50%); } diff --git a/frontend/views/components/ProfileCard.vue b/frontend/views/components/ProfileCard.vue index 4ef0cd4a60..ea3ca6e5ee 100644 --- a/frontend/views/components/ProfileCard.vue +++ b/frontend/views/components/ProfileCard.vue @@ -176,12 +176,15 @@ export default ({ ) }, toggleTooltip () { - this.$refs.tooltip.toggle() + this.$refs.tooltip?.toggle() }, async sendMessage () { const chatRoomID = this.ourGroupDirectMessageFromUserIds(this.contractID) if (!chatRoomID) { - await this.createDirectMessage(this.contractID) + const freshChatRoomID = await this.createDirectMessage(this.contractID) + if (freshChatRoomID) { + this.redirect(freshChatRoomID) + } } else { if (!this.ourGroupDirectMessages[chatRoomID].visible) { this.setDMVisibility(chatRoomID, true) diff --git a/frontend/views/components/Tooltip.vue b/frontend/views/components/Tooltip.vue index 778444c9d7..a045e878bc 100644 --- a/frontend/views/components/Tooltip.vue +++ b/frontend/views/components/Tooltip.vue @@ -162,9 +162,9 @@ export default ({ switch (this.direction) { case 'right': - x = '100%' + x = `${spacing}px` y = '-50%' - absPosition = { top: '50%', right: `-${spacing}px` } + absPosition = { top: '50%', left: '100%' } break case 'left': x = '-100%' @@ -177,9 +177,9 @@ export default ({ absPosition = { bottom: `-${spacing}px` } break case 'bottom-right': - x = 0 + x = '-100%' y = '100%' - absPosition = { bottom: `-${spacing}px`, right: 0 } + absPosition = { bottom: `-${spacing}px`, left: '100%' } break case 'top': x = '-50%' @@ -324,6 +324,7 @@ export default ({ if (this.triggerElementSelector) { this.triggerDOM.style.cursor = 'pointer' + this.triggerDOM.style.position = 'relative' } }, beforeDestory () { @@ -388,6 +389,10 @@ export default ({ } } +.c-anchored-tooltip { + width: max-content; +} + .c-background { position: absolute; z-index: $zindex-tooltip - 1; diff --git a/frontend/views/containers/chatroom/ChatMain.vue b/frontend/views/containers/chatroom/ChatMain.vue index 637cbca5fd..69547e370b 100644 --- a/frontend/views/containers/chatroom/ChatMain.vue +++ b/frontend/views/containers/chatroom/ChatMain.vue @@ -128,7 +128,7 @@ import Vue from 'vue' import Avatar from '@components/Avatar.vue' import InfiniteLoading from 'vue-infinite-loading' import Message from './Message.vue' -import MessageInteractive from './MessageInteractive.vue' +import MessageInteractive, { interactiveMessage } from './MessageInteractive.vue' import MessageNotification from './MessageNotification.vue' import MessagePoll from './MessagePoll.vue' import ConversationGreetings from '@containers/chatroom/ConversationGreetings.vue' @@ -137,7 +137,7 @@ import ViewArea from './ViewArea.vue' import Emoticons from './Emoticons.vue' import TouchLinkHelper from './TouchLinkHelper.vue' import DragActiveOverlay from './file-attachment/DragActiveOverlay.vue' -import { MESSAGE_TYPES, MESSAGE_VARIANTS, CHATROOM_ACTIONS_PER_PAGE } from '@model/contracts/shared/constants.js' +import { MESSAGE_TYPES, MESSAGE_VARIANTS, CHATROOM_ACTIONS_PER_PAGE, CHATROOM_MEMBER_MENTION_SPECIAL_CHAR } from '@model/contracts/shared/constants.js' import { CHATROOM_EVENTS, NEW_CHATROOM_UNREAD_POSITION, DELETE_ATTACHMENT_FEEDBACK } from '@utils/events.js' import { findMessageIdx } from '@model/contracts/shared/functions.js' import { proximityDate, MINS_MILLIS } from '@model/contracts/shared/time.js' @@ -616,9 +616,20 @@ export default ({ this.handleSendMessage(message.text, message.attachments, message.replyingMessage) }, replyMessage (message) { - const { text, hash } = message - this.ephemeral.replyingMessage = { text, hash } - this.ephemeral.replyingTo = this.who(message) + const { text, hash, type } = message + + if (type === MESSAGE_TYPES.INTERACTIVE) { + const proposal = message.proposal + + this.ephemeral.replyingMessage = { + text: interactiveMessage(proposal, { from: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${proposal.creatorID}` }), + hash + } + this.ephemeral.replyingTo = L('Proposal notification') + } else { + this.ephemeral.replyingMessage = { text, hash } + this.ephemeral.replyingTo = this.who(message) + } }, editMessage (message, newMessage) { message.text = newMessage diff --git a/frontend/views/containers/chatroom/DMMixin.js b/frontend/views/containers/chatroom/DMMixin.js index 889184866e..fdf1f117ee 100644 --- a/frontend/views/containers/chatroom/DMMixin.js +++ b/frontend/views/containers/chatroom/DMMixin.js @@ -22,6 +22,8 @@ const DMMixin: Object = { try { const identityContractID = this.ourIdentityContractId const currentGroupId = this.currentGroupId + let dmChatRoomId + await sbp('gi.actions/identity/createDirectMessage', { contractID: identityContractID, data: { currentGroupId, memberIDs }, @@ -33,10 +35,13 @@ const DMMixin: Object = { // state ('pending') and we'll set the chatroom ID when the // contract is loaded. // This is done in the JOINED_CHATROOM event. - sbp('state/vuex/commit', 'setPendingChatRoomId', { chatRoomID: message.contractID(), groupID: currentGroupId }) + dmChatRoomId = message.contractID() + sbp('state/vuex/commit', 'setPendingChatRoomId', { chatRoomID: dmChatRoomId, groupID: currentGroupId }) } } }) + + return dmChatRoomId } catch (err) { console.error('[DMMixin.js] Failed to create a new chatroom', err) await sbp('gi.ui/prompt', { diff --git a/frontend/views/containers/chatroom/MessageActions.vue b/frontend/views/containers/chatroom/MessageActions.vue index 9cc794a174..be7c9ff626 100644 --- a/frontend/views/containers/chatroom/MessageActions.vue +++ b/frontend/views/containers/chatroom/MessageActions.vue @@ -23,7 +23,7 @@ menu-parent.c-message-menu(ref='menu') i.icon-pencil-alt tooltip( - v-if='isText' + v-if='isReplyable' direction='top' :text='L("Reply")' ) @@ -107,6 +107,9 @@ export default ({ isPoll () { return this.type === MESSAGE_TYPES.POLL }, + isReplyable () { + return [MESSAGE_TYPES.TEXT, MESSAGE_TYPES.INTERACTIVE].includes(this.type) + }, isPinnable () { return this.isText || this.isPoll }, @@ -130,9 +133,9 @@ export default ({ conditionToShow: !this.isDesktopScreen && this.isEditable }, { name: L('Reply'), - action: 'Reply', + action: 'reply', icon: 'reply', - conditionToShow: !this.isDesktopScreen && this.isText + conditionToShow: !this.isDesktopScreen && this.isReplyable }, { name: L('Retry'), action: 'retry', diff --git a/frontend/views/containers/chatroom/MessageInteractive.vue b/frontend/views/containers/chatroom/MessageInteractive.vue index 3d3b9cb871..4687121475 100644 --- a/frontend/views/containers/chatroom/MessageInteractive.vue +++ b/frontend/views/containers/chatroom/MessageInteractive.vue @@ -1,5 +1,8 @@