diff --git a/.all-contributorsrc b/.all-contributorsrc
index 8ce230c9f9..d4347104e8 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -571,6 +571,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "darigovresearch",
+ "name": "Darigov Research",
+ "avatar_url": "https://avatars.githubusercontent.com/u/30328618?v=4",
+ "profile": "https://www.darigovresearch.com/",
+ "contributions": [
+ "doc"
+ ]
}
],
"projectName": "community-platform",
diff --git a/README.md b/README.md
index 65409f17c8..6a1f24a9ae 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,7 @@ Thanks go to these wonderful people ([emoji key](https://allcontributors.org/doc
Ignas π» |
MΓ‘rio Nunes π» |
Kevin Masson π» |
+ Darigov Research π |
diff --git a/packages/documentation/docs/Contributing/start-contributing.md b/packages/documentation/docs/Contributing/start-contributing.md
index 0112c3c802..6a39fffe7c 100644
--- a/packages/documentation/docs/Contributing/start-contributing.md
+++ b/packages/documentation/docs/Contributing/start-contributing.md
@@ -5,46 +5,46 @@ title: Start Contributing
## What is my role?
-By default we consider everyone that submits a PR a contributor. People that contribute regularly and want to get more involved can become maintainers to help new contributors. People here for the long term are core maintainers and work on the core of the platform. Here is an overview of the roles
+By default we consider everyone that submits a pull request (PR) to be a contributor. People that contribute regularly and want to get more involved can become maintainers to help new contributors. People here for the long term are core maintainers and work on the core of the platform. Here is an overview of the roles
-| Role | Payment | Requirements | Tasks |
-| --------------------- | ------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
-| **π€ Contributor** | Bounty System | Submit a PR | Resolves issues by submitting Pull Requests |
-| **β‘οΈ Maintainer** | Bounty System | Minimum 3 merged PR's + chat with core maintainer | Review basic PR's, help contributors, improve documentation |
-| **πͺ Core Maintainer** | Hourly scale | Minimum 3 months maintainer | Review complex PR's, Improve code quality, Devops, optimisations, Security, general updates, Documentation |
+| Role | Payment | Requirements | Tasks |
+| ---------------------- | ------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
+| **π€ Contributor** | Bounty System | Submit a PR | Resolves issues by submitting pull requests |
+| **β‘οΈ Maintainer** | Bounty System | Minimum 3 merged PR's + chat with core maintainer | Review basic PR's, help contributors & improve documentation |
+| **π§ Core Maintainer** | Hourly scale | Minimum 3 months maintainer | Review complex PR's, improve code quality, devops, optimisations, security, general updates & documentation |
-### π€ Contributors to do:
+### π€ Things that Contributors do:
#### Pick up issues
1. Check our [documentation](/) to run it locally.
2. Pick an open [issue](https://github.com/ONEARMY/community-platform/issues).
3. If there is a [bounty](/Contributing/bounties) label on it you can claim a reward.
-4. Read our [contribution guidelines](https://github.com/ONEARMY/community-platform/blob/master/CONTRIBUTING.md)
+4. Read our [contribution guidelines](https://github.com/ONEARMY/community-platform/blob/master/CONTRIBUTING.md).
5. Bonus: Add [tests](/Testing/end-to-end).
-### β‘οΈ Maintainers to do:
+### β‘οΈ Things that Maintainers do:
#### Review incoming code from Contributors
-1. Validate if code is clean
-2. Validate if the [project structure](https://github.com/ONEARMY/community-platform/blob/master/CONTRIBUTING.md#--project-structure) is correct
-3. Check out user profile to see who is behind the commit
-4. Add them to our [contributors list](#recognising-contributors)
-5. Make sure [Lint commit](https://github.com/ONEARMY/community-platform/blob/master/CONTRIBUTING.md#--commit-style-guide) message is correct
+1. Validate if code is clean.
+2. Validate if the [project structure](https://github.com/ONEARMY/community-platform/blob/master/CONTRIBUTING.md#--project-structure) is correct.
+3. Check out user profile to see who is behind the commit.
+4. Add them to our [contributors list](#recognising-contributors).
+5. Make sure [Lint commit](https://github.com/ONEARMY/community-platform/blob/master/CONTRIBUTING.md#--commit-style-guide) message is correct.
6. Make sure all checks are passed and help to resolve errors.
7. If there is a [bounty](/Contributing/bounties) label on the PR you can get it for reviewing.
-8. Bonus: Ask for integrating tests
+8. Bonus: Ask for integrating tests.
-### πͺ Core Maintainer to do:
+### π§ Things that Core Maintainers do:
-#### Review complex PRs & Releasing Changes
+#### Review complex PRs & release changes
-The workflow for deploying code changes from development to production environments. It ensures proper review, testing, and versioning before reaching production sites.
+There is a workflow for deploying code changes from development to production environments. It ensures proper review, testing, and versioning before reaching production sites.
The steps are as follows:
-- Pull Request (PR) is merged into the `master` branch.
+- The PR is merged into the `master` branch.
- `master` branch triggers an automated build and deployment to development environments.
- A Release PR is automatically raised to merge `master` into the `production` branch.
- Manual approval is required by a maintainer, then merge Release PR into `production`.
@@ -58,25 +58,25 @@ We have adopted [all contributors](https://allcontributors.org/) and their tooli
After merging a new contributors PR:
-1. Add a comment to the merged PR mentioning the bot, contributor and their contribution [type](https://allcontributors.org/docs/en/emoji-key), for example: `@all-contributors add @username for code`
-2. A PR will be automatically raised, [example](https://github.com/ONEARMY/community-platform/pull/1952)
-3. The PR raised by the All Contributors bot will need to be merged with admin privileges as the required CI skips are deliberately skipped.
+1. Add a comment to the merged PR mentioning the bot, contributor and their contribution [type](https://allcontributors.org/docs/en/emoji-key), for example: `@all-contributors add @username for code`.
+2. A PR will be automatically raised, [example](https://github.com/ONEARMY/community-platform/pull/1952).
+3. The PR raised by the `All Contributors` bot will need to be merged with admin privileges as the required CI skips are deliberately skipped.
### Payment
-Each role get paid a bit differently. Contributors and maintainers get paid according to the [Bounty system](/Contributing/bounties). For core maintainers there is a separate hourly pay scale. Aimed at developers who help a bit more consistently at around 2-3h per week. If you're interested in these roles then feel free to reach out on [Discord](https://discord.gg/gJ7Yyk4).
+Each role gets paid a bit differently. Contributors and maintainers get paid according to the [Bounty system](/Contributing/bounties). For core maintainers there is a separate hourly pay scale. Aimed at developers who help a bit more consistently at around 2-3 hours per week. If you're interested in these roles then feel free to reach out on [Discord](https://discord.gg/gJ7Yyk4).
## Onboarding checklist
-| Tasks | π€ Contributor | β‘οΈ Maintainer | πͺ Core Maintainer |
-| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -------------- | ----------------- |
-| Invite to [Discord](https://discord.gg/gJ7Yyk4) #development | βοΈ | βοΈ | βοΈ |
-| Send link to [bounty](/Contributing/bounties) system | βοΈ | βοΈ | βοΈ |
-| Add GitHub [maintainer permissions](https://github.com/ONEARMY/community-platform/settings/access) | | βοΈ | βοΈ |
-| Add maintainer status using [All contributors ](#recognising-contributors) | | βοΈ | βοΈ |
-| Get on a video call | | βοΈ | βοΈ |
-| Explain hourly rate vs bounty system | | βοΈ | βοΈ |
-| Add GitHub [core maintainer permissions](https://github.com/ONEARMY/community-platform/settings/access) | | | βοΈ |
-| Add core-maintainer status using [All contributors ](#recognising-contributors) | | | βοΈ |
-| Invite to Google Analytics | | | βοΈ |
-| Invite to **Firebase** projects: Precious Plastic PROD, Precious Plastic DEV, Project Kamp PROD, Project Kamp DEV, Fixing Fashion PROD, Fixing Fashion DEV | | | βοΈ |
+| Tasks | π€ Contributor | β‘οΈ Maintainer | π§ Core Maintainer |
+| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -------------- | ------------------ |
+| Invite to [Discord](https://discord.gg/gJ7Yyk4) #development | βοΈ | βοΈ | βοΈ |
+| Send link to [bounty](/Contributing/bounties) system | βοΈ | βοΈ | βοΈ |
+| Add GitHub [maintainer permissions](https://github.com/ONEARMY/community-platform/settings/access) | | βοΈ | βοΈ |
+| Add maintainer status using [All contributors ](#recognising-contributors) | | βοΈ | βοΈ |
+| Get on a video call | | βοΈ | βοΈ |
+| Explain hourly rate vs bounty system | | βοΈ | βοΈ |
+| Add GitHub [core maintainer permissions](https://github.com/ONEARMY/community-platform/settings/access) | | | βοΈ |
+| Add core-maintainer status using [All contributors ](#recognising-contributors) | | | βοΈ |
+| Invite to Google Analytics | | | βοΈ |
+| Invite to **Firebase** projects: Precious Plastic PROD, Precious Plastic DEV, Project Kamp PROD, Project Kamp DEV, Fixing Fashion PROD, Fixing Fashion DEV | | | βοΈ |
diff --git a/src/pages/Question/QuestionPage.tsx b/src/pages/Question/QuestionPage.tsx
index e9bc59c899..accef14300 100644
--- a/src/pages/Question/QuestionPage.tsx
+++ b/src/pages/Question/QuestionPage.tsx
@@ -1,4 +1,9 @@
-import { Loader, ModerationStatus, UsefulStatsButton } from 'oa-components'
+import {
+ Loader,
+ ModerationStatus,
+ UsefulStatsButton,
+ FollowButton,
+} from 'oa-components'
import { useState, useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import type { IQuestion } from 'src/models'
@@ -13,20 +18,27 @@ export const QuestionPage = () => {
const [isLoading, setIsLoading] = useState(true)
const [question, setQuestion] = useState()
const [isEditable, setIsEditable] = useState(false)
+ const [hasUserSubscribed, setHasUserSubscribed] = useState(false)
useEffect(() => {
const fetchQuestions = async () => {
if (slug) {
- const question: any = await store.fetchQuestionBySlug(slug)
- store.activeQuestionItem = question || null
+ const foundQuestion: any = await store.fetchQuestionBySlug(slug)
+ store.activeQuestionItem = foundQuestion || null
if (isLoading) {
- setQuestion(question || null)
+ setQuestion(foundQuestion || null)
if (store.activeUser) {
- setIsEditable(isAllowedToEditContent(question, store.activeUser))
+ setIsEditable(
+ isAllowedToEditContent(foundQuestion, store.activeUser),
+ )
}
}
+
+ setHasUserSubscribed(
+ foundQuestion?.subscribers?.includes(store.activeUser?.userName),
+ )
}
setIsLoading(false)
@@ -52,19 +64,36 @@ export const QuestionPage = () => {
}
}
+ const isLoggedIn = store.activeUser ? true : false
+ const onFollowClick = () => {
+ if (question) {
+ store.toggleSubscriberStatusByUserName(
+ question._id,
+ store.activeUser?.userName,
+ )
+ setHasUserSubscribed(!hasUserSubscribed)
+ }
+ return null
+ }
+
return (
{isLoading ? (
) : question ? (
-
+
+
{
})
})
+ describe('Follow', () => {
+ it('displays following status', async () => {
+ let wrapper
+ const user = FactoryUser()
+ const question = FactoryQuestionItem({
+ subscribers: [user.userName],
+ })
+ const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question)
+ useQuestionStore.mockReturnValue({
+ ...mockQuestionStore,
+ activeUser: user,
+ fetchQuestionBySlug: mockFetchQuestionBySlug,
+ })
+
+ await act(async () => {
+ wrapper = (await renderFn(`/questions/${question.slug}`)).wrapper
+ })
+
+ await waitFor(() => {
+ expect(wrapper.getByText('Following')).toBeInTheDocument()
+ })
+ })
+
+ it('supports follow behaviour', async () => {
+ let wrapper
+ const question = FactoryQuestionItem()
+ const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question)
+ useQuestionStore.mockReturnValue({
+ ...mockQuestionStore,
+ fetchQuestionBySlug: mockFetchQuestionBySlug,
+ })
+
+ await act(async () => {
+ wrapper = (await renderFn(`/questions/${question.slug}`)).wrapper
+ })
+
+ expect(wrapper.getByText('Follow')).toBeInTheDocument()
+ })
+ })
+
it('does not show Edit call to action', async () => {
let wrapper
mockActiveUser = FactoryUser()
diff --git a/src/stores/Question/question.store.test.tsx b/src/stores/Question/question.store.test.tsx
index 7a71f4fcc4..4826136825 100644
--- a/src/stores/Question/question.store.test.tsx
+++ b/src/stores/Question/question.store.test.tsx
@@ -16,6 +16,12 @@ const factory = async () => {
return newValue
})
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ store.db.update.mockImplementation((newValue) => {
+ return newValue
+ })
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
store.db.getWhere.mockImplementation(async () => {})
@@ -28,6 +34,9 @@ const factory = async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
getWhereFn: store.db.getWhere,
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ updateFn: store.db.update,
}
}
@@ -158,4 +167,24 @@ describe('question.store', () => {
expect(questionDoc).toStrictEqual(newQuestion)
})
})
+
+ describe('toggleSubscriberStatusByUserName', () => {
+ it('adds user to subscribers list', async () => {
+ const { store, updateFn } = await factory()
+ const newQuestion = FactoryQuestionItem({
+ title: 'Question title',
+ subscribers: [],
+ })
+
+ // Act
+ await store.toggleSubscriberStatusByUserName(newQuestion._id, 'user1')
+
+ expect(updateFn).toBeCalledWith(
+ expect.objectContaining({
+ _id: newQuestion._id,
+ subscribers: ['user1'],
+ }),
+ )
+ })
+ })
})
diff --git a/src/stores/Question/question.store.tsx b/src/stores/Question/question.store.tsx
index 7c87ad4f6f..a3ebec7422 100644
--- a/src/stores/Question/question.store.tsx
+++ b/src/stores/Question/question.store.tsx
@@ -12,6 +12,7 @@ import {
formatLowerNoSpecial,
randomID,
} from 'src/utils/helpers'
+import { toggleDocSubscriberStatusByUserName } from '../common/toggleDocSubscriberStatusByUserName'
const COLLECTION_NAME = 'questions'
@@ -115,6 +116,15 @@ export class QuestionStore extends ModuleStore {
: ''
}
+ public async toggleSubscriberStatusByUserName(docId, userName) {
+ return toggleDocSubscriberStatusByUserName(
+ this.db,
+ COLLECTION_NAME,
+ docId,
+ userName,
+ )
+ }
+
private async _getQuestionItemBySlug(
slug: string,
): Promise {
diff --git a/src/stores/Research/research.store.test.ts b/src/stores/Research/research.store.test.ts
index 438bd2d5f2..061b69fe74 100644
--- a/src/stores/Research/research.store.test.ts
+++ b/src/stores/Research/research.store.test.ts
@@ -781,11 +781,10 @@ describe('research.store', () => {
describe('Subscribe', () => {
it('adds subscriber to the research article', async () => {
- const { store, researchItem, setFn } = await factoryResearchItemFormInput(
- {
+ const { store, researchItem, updateFn } =
+ await factoryResearchItemFormInput({
subscribers: ['existing-subscriber'],
- },
- )
+ })
// Act
await store.addSubscriberToResearchArticle(
@@ -794,37 +793,23 @@ describe('research.store', () => {
)
// Assert
- expect(setFn).toHaveBeenCalledTimes(1)
- const [newResearchItem] = setFn.mock.calls[0]
+ expect(updateFn).toHaveBeenCalledTimes(1)
+ const [newResearchItem] = updateFn.mock.calls[0]
expect(newResearchItem).toEqual(
expect.objectContaining({
- subscribers: ['an-interested-user', 'existing-subscriber'],
+ subscribers: expect.arrayContaining([
+ 'an-interested-user',
+ 'existing-subscriber',
+ ]),
}),
)
})
- it('does not add a duplicate subscriber to the research article', async () => {
- const { store, researchItem, setFn } = await factoryResearchItemFormInput(
- {
- subscribers: ['a-very-interested-user'],
- },
- )
-
- // Act
- await store.addSubscriberToResearchArticle(
- researchItem._id,
- 'a-very-interested-user',
- )
-
- expect(setFn).not.toBeCalled()
- })
-
it('removes subscriber from the research article', async () => {
- const { store, researchItem, setFn } = await factoryResearchItemFormInput(
- {
+ const { store, researchItem, updateFn } =
+ await factoryResearchItemFormInput({
subscribers: ['long-term-subscriber', 'remove-me'],
- },
- )
+ })
// Act
await store.removeSubscriberFromResearchArticle(
@@ -833,8 +818,8 @@ describe('research.store', () => {
)
// Assert
- expect(setFn).toHaveBeenCalledTimes(1)
- const [newResearchItem] = setFn.mock.calls[0]
+ expect(updateFn).toHaveBeenCalledTimes(1)
+ const [newResearchItem] = updateFn.mock.calls[0]
expect(newResearchItem).toEqual(
expect.objectContaining({
subscribers: ['long-term-subscriber'],
diff --git a/src/stores/Research/research.store.tsx b/src/stores/Research/research.store.tsx
index 9fd43e8055..fbfff16cbb 100644
--- a/src/stores/Research/research.store.tsx
+++ b/src/stores/Research/research.store.tsx
@@ -33,6 +33,7 @@ import {
ItemSortingOption,
} from '../common/FilterSorterDecorator/FilterSorterDecorator'
import { toggleDocUsefulByUser } from '../common/toggleDocUsefulByUser'
+import { toggleDocSubscriberStatusByUserName } from '../common/toggleDocSubscriberStatusByUserName'
const COLLECTION_NAME = 'research'
@@ -193,19 +194,7 @@ export class ResearchStore extends ModuleStore {
docId: string,
userId: string,
): Promise {
- const dbRef = this.db.collection(COLLECTION_NAME).doc(docId)
-
- const researchData = await toJS(dbRef.get('server'))
- if (researchData && !(researchData?.subscribers || []).includes(userId)) {
- const updatedItem = await this._updateResearchItem(dbRef, {
- ...researchData,
- subscribers: [userId].concat(researchData?.subscribers || []),
- })
-
- if (updatedItem) {
- this.setActiveResearchItemBySlug(updatedItem.slug)
- }
- }
+ await this._toggleSubscriber(docId, userId)
return
}
@@ -214,22 +203,7 @@ export class ResearchStore extends ModuleStore {
docId: string,
userId: string,
): Promise {
- const dbRef = this.db.collection(COLLECTION_NAME).doc(docId)
-
- const researchData = await toJS(dbRef.get('server'))
- if (researchData) {
- const updatedItem = await this._updateResearchItem(dbRef, {
- ...researchData,
- subscribers: (researchData?.subscribers || []).filter(
- (id) => id !== userId,
- ),
- })
-
- if (updatedItem) {
- this.setActiveResearchItemBySlug(updatedItem.slug)
- }
- }
-
+ await this._toggleSubscriber(docId, userId)
return
}
@@ -1126,6 +1100,21 @@ export class ResearchStore extends ModuleStore {
return undefined
}
+
+ private async _toggleSubscriber(docId, userId) {
+ const updatedItem = await toggleDocSubscriberStatusByUserName(
+ this.db,
+ COLLECTION_NAME,
+ docId,
+ userId,
+ )
+
+ if (updatedItem) {
+ this.setActiveResearchItemBySlug(updatedItem.slug)
+ }
+
+ return updatedItem
+ }
}
interface IResearchUploadStatus {
diff --git a/src/stores/common/toggleDocSubscriberStatusByUserName.ts b/src/stores/common/toggleDocSubscriberStatusByUserName.ts
new file mode 100644
index 0000000000..09bb542928
--- /dev/null
+++ b/src/stores/common/toggleDocSubscriberStatusByUserName.ts
@@ -0,0 +1,32 @@
+import { logger } from 'src/logger'
+import type { IQuestion } from 'src/models'
+import type { DatabaseV2 } from '../databaseV2'
+import type { DBEndpoint } from '../databaseV2/endpoints'
+
+export const toggleDocSubscriberStatusByUserName = async (
+ db: DatabaseV2,
+ collectionName: DBEndpoint,
+ docId: string,
+ userName: string,
+) => {
+ const dbRef = db.collection(collectionName).doc(docId)
+
+ if (!dbRef) {
+ logger.warn('Unable to find document', { docId })
+ return null
+ }
+
+ const doc = await dbRef.get()
+
+ let subscribers = doc?.subscribers || []
+
+ if (doc?.subscribers?.includes(userName)) {
+ subscribers = subscribers.filter((s) => s !== userName)
+ } else {
+ subscribers.push(userName)
+ }
+
+ dbRef.update({ _id: docId, subscribers } as any)
+
+ return await dbRef.get()
+}