From 6f0b131248cb3278b3c7e96e4d3a1cdb49e6c677 Mon Sep 17 00:00:00 2001 From: Lilian Desvaux de Marigny Date: Wed, 28 Sep 2022 14:29:03 +0200 Subject: [PATCH] feat: email notifications feature (#1012) * feat: add newsletter widget on profile * feat: add popup to configure newsletter * style: add darkmode and responsive to newsletter components * feat: add notification page in settings to manage all newsletters * feat: added authors list to newsletter settings * feat: connect newsletter popup to authors in notifications settings page * fix: rebuild newsletter process with different UI * feat: add new email address section in newsletter popup * feat: added list of emails, create subscription, list authors (#1114) * feat: added list of emails, create subscription, list authors * feat: ui binding for manage all newsletters * feat: error handling in email ui * feat: refetching email subs on create * style: polish newsletter popup * fix: refetch newsletter on delete * style: polish status and delete alert * fix: newsletter type * fix: hide newsletter widget when unauth * style: change newsletter into notification * fix: add missing popup portal to settings layout * feat: upload static images used in emails (#1142) * ci: add cure53 deploy * fix: add cure53 ipfs addr to bootstrap nodes * fix: updated socials interface and type (#1169) * chore: upgrade near-api-js to v1.0.0 (#1155) * fix: prevent duplicate posts and clearing of editor (#1170) * chore: remove cure53 bootstrap node Co-authored-by: Irshad PI Co-authored-by: Irshad PI Co-authored-by: jack dishman Co-authored-by: David Grisham Co-authored-by: Rahul Trivedi <32861078+rahultrivedi180@users.noreply.github.com> Co-authored-by: Christos Panagiotakopoulos --- .github/configs/cure53.env | 17 + .github/workflows/ci-release-development.yml | 20 +- src/backend/emails.ts | 119 ++++++ src/components/NewsletterPreview.vue | 175 +++++++++ src/components/ProfilePreviewCard.vue | 66 ++++ src/components/icons/Bell.vue | 8 + src/components/popups/AddNewsletter.vue | 348 ++++++++++++++++++ .../popups/ConfigureNewsletterPopup.vue | 117 ++++++ src/components/widgets/EmailNewsletter.vue | 105 ++++++ src/layouts/profile.vue | 41 +++ src/layouts/settings.vue | 39 ++ src/pages/settings/notifications.vue | 69 ++++ src/static/images/blogchain-logo.png | Bin 0 -> 5399 bytes src/static/images/email-icon.png | Bin 0 -> 1190 bytes 14 files changed, 1111 insertions(+), 13 deletions(-) create mode 100644 .github/configs/cure53.env create mode 100644 src/backend/emails.ts create mode 100644 src/components/NewsletterPreview.vue create mode 100644 src/components/ProfilePreviewCard.vue create mode 100644 src/components/icons/Bell.vue create mode 100644 src/components/popups/AddNewsletter.vue create mode 100644 src/components/popups/ConfigureNewsletterPopup.vue create mode 100644 src/components/widgets/EmailNewsletter.vue create mode 100644 src/pages/settings/notifications.vue create mode 100644 src/static/images/blogchain-logo.png create mode 100644 src/static/images/email-icon.png diff --git a/.github/configs/cure53.env b/.github/configs/cure53.env new file mode 100644 index 000000000..fb97757e4 --- /dev/null +++ b/.github/configs/cure53.env @@ -0,0 +1,17 @@ +CONTRACT_NAME=dev-1657702549987-47675900699610 +HCAPTCHA_SITE_KEY="7e68122f-db5e-4100-bdab-0240658070d9" + +CAPSULE_SERVER=https://cure53.blogchain.app/server +DOMAIN=https://cure53.blogchain.app +STRIPE_PUBLISHABLE_KEY="pk_test_51I81pBCPCJ3FaYLGnUrPUMxipudV7gWWA7qAiqIVMAqnULA4a2uluUgBQxX8yKzAe2iGYOoSMX2rSbF45wtKlhXI00Olk8hJmc" + +ORBIT_URL=https://cure53.blogchain.app/orbit +DEBUG=true + +# Discord +TORUS_DISCORD_VERIFIER="audit-verifier-august-discord" +TORUS_DISCORD_CLIENTID="906210984396468275" + +# Google +TORUS_GOOGLE_VERIFIER="audit-verifier-august" +TORUS_GOOGLE_CLIENTID="653379121360-j8t9ua763vfvd86d1qjguonhrgqvkigo.apps.googleusercontent.com" diff --git a/.github/workflows/ci-release-development.yml b/.github/workflows/ci-release-development.yml index d47f1b6cd..553f7efee 100644 --- a/.github/workflows/ci-release-development.yml +++ b/.github/workflows/ci-release-development.yml @@ -4,15 +4,12 @@ on: tags: - alpha - payments - jobs: - install-dependencies: runs-on: ubuntu-latest container: image: node:16-alpine steps: - - uses: actions/checkout@v1 - name: install tar (for actions/cache) @@ -44,7 +41,6 @@ jobs: container: image: node:16-alpine steps: - - uses: actions/checkout@v1 - uses: actions/download-artifact@v2 @@ -62,7 +58,6 @@ jobs: container: image: node:16-alpine steps: - - name: install git run: apk add --no-cache git @@ -104,8 +99,8 @@ jobs: - name: Upload build as release asset uses: ncipollo/release-action@v1 with: - artifacts: ${{ env.BUILD_NAME }}.tar.gz - token: ${{ secrets.GITREPO_TOKEN }} + artifacts: ${{ env.BUILD_NAME }}.tar.gz + token: ${{ secrets.GITREPO_TOKEN }} allowUpdates: true deploy: @@ -114,7 +109,6 @@ jobs: container: image: lexauw/ansible-alpine:latest steps: - - name: install git run: apk add --no-cache git @@ -135,9 +129,9 @@ jobs: - name: Deploy updated images run: ./.github/scripts/deploy.sh env: - DEPLOY_ENV: ${{ env.DEPLOY_ENV }} - RELEASE_ID: ${{ env.RELEASE_ID }} - RELEASE_TAG: ${{ env.RELEASE_TAG }} - GITHUB_USER: ${{ secrets.GITREPO_USER }} - GITHUB_TOKEN: ${{ secrets.GITREPO_TOKEN }} + DEPLOY_ENV: ${{ env.DEPLOY_ENV }} + RELEASE_ID: ${{ env.RELEASE_ID }} + RELEASE_TAG: ${{ env.RELEASE_TAG }} + GITHUB_USER: ${{ secrets.GITREPO_USER }} + GITHUB_TOKEN: ${{ secrets.GITREPO_TOKEN }} ANSIBLE_PRIVATE_KEY: ${{ secrets.ANSIBLE_PRIVATE_KEY }} diff --git a/src/backend/emails.ts b/src/backend/emails.ts new file mode 100644 index 000000000..7e1c8531b --- /dev/null +++ b/src/backend/emails.ts @@ -0,0 +1,119 @@ +import { AxiosError } from 'axios' +import { genericRequest } from './utilities/request' + +export enum EmailSubscriptionMode { + AllPosts = `AllPosts`, + Topics = `Topics`, +} + +export interface IEmailSubscription { + _id?: string + username?: string + authorID: string + email: string + verified: boolean + createdAt: Date + updatedAt: Date + topics: string[] + mode: EmailSubscriptionMode +} + +export interface UserEmail { + email: string + authorSubbed: boolean +} + +export async function startEmailSubscription( + authorID: string, + email: string, + topics: string[] = [], + mode: EmailSubscriptionMode = EmailSubscriptionMode.AllPosts, + username?: string, +) { + try { + const body = { + authorID, + email, + topics, + mode, + } + const response = await genericRequest({ + method: `post`, + path: `/emails/auth/subscribe`, + username, + body, + }) + return response + } catch (err) { + if (err instanceof AxiosError && err.response) { + throw new Error(err.response.data?.error ?? err.message) + } + throw new Error(`Network error: ${err}`) + } +} + +export async function listAllAuthors(username: string) { + try { + const response = await genericRequest<{ data: Array }>({ + method: `get`, + path: `/emails/authors`, + username, + }) + return response.data + } catch (err) { + if (err instanceof AxiosError && err.response) { + throw new Error(err.response.data?.error ?? err.message) + } + throw new Error(`Network error: ${err}`) + } +} + +export async function listForAuthor(authorID: string, username: string) { + try { + const response = await genericRequest<{ authorID: string }, { data: Array }>({ + method: `get`, + path: `/emails/subscribed`, + params: { authorID }, + username, + }) + return response.data + } catch (err) { + if (err instanceof AxiosError && err.response) { + throw new Error(err.response.data?.error ?? err.message) + } + throw new Error(`Network error: ${err}`) + } +} + +export async function listEmails(username: string, authorID: string) { + try { + const response = await genericRequest<{ authorID: string }, { data: Array }>({ + method: `get`, + path: `/emails/list`, + username, + params: { authorID }, + }) + return response.data + } catch (err) { + if (err instanceof AxiosError && err.response) { + throw new Error(err.response.data?.error ?? err.message) + } + throw new Error(`Network error: ${err}`) + } +} + +export async function deleteSubscription(username: string, id: string) { + try { + const response = await genericRequest({ + method: `delete`, + path: `/emails/${id}`, + username, + }) + return response.data + } catch (err) { + if (err instanceof AxiosError && err.response) { + throw new Error(err.response.data?.error ?? err.message) + } + throw new Error(`Network error: ${err}`) + } +} diff --git a/src/components/NewsletterPreview.vue b/src/components/NewsletterPreview.vue new file mode 100644 index 000000000..b1bf649af --- /dev/null +++ b/src/components/NewsletterPreview.vue @@ -0,0 +1,175 @@ + + + + diff --git a/src/components/ProfilePreviewCard.vue b/src/components/ProfilePreviewCard.vue new file mode 100644 index 000000000..73843cccb --- /dev/null +++ b/src/components/ProfilePreviewCard.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/components/icons/Bell.vue b/src/components/icons/Bell.vue new file mode 100644 index 000000000..58b4b4e2b --- /dev/null +++ b/src/components/icons/Bell.vue @@ -0,0 +1,8 @@ + diff --git a/src/components/popups/AddNewsletter.vue b/src/components/popups/AddNewsletter.vue new file mode 100644 index 000000000..e0b963e75 --- /dev/null +++ b/src/components/popups/AddNewsletter.vue @@ -0,0 +1,348 @@ + + + \ No newline at end of file diff --git a/src/components/popups/ConfigureNewsletterPopup.vue b/src/components/popups/ConfigureNewsletterPopup.vue new file mode 100644 index 000000000..12bcf5076 --- /dev/null +++ b/src/components/popups/ConfigureNewsletterPopup.vue @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/src/components/widgets/EmailNewsletter.vue b/src/components/widgets/EmailNewsletter.vue new file mode 100644 index 000000000..5c13319fc --- /dev/null +++ b/src/components/widgets/EmailNewsletter.vue @@ -0,0 +1,105 @@ + + + \ No newline at end of file diff --git a/src/layouts/profile.vue b/src/layouts/profile.vue index d7b6d3816..f656f246e 100644 --- a/src/layouts/profile.vue +++ b/src/layouts/profile.vue @@ -49,6 +49,13 @@ :email="visitProfile.email" :website="visitProfile.website" /> + + + @@ -156,6 +171,7 @@ import { AxiosError } from 'axios' import ProfileWidget from '@/components/widgets/Profile.vue' import FollowersWidget from '@/components/widgets/Followers.vue' import MutualFollowersWidget from '@/components/widgets/MutualFollowers.vue' +import EmailNewsletterWidget from '@/components/widgets/EmailNewsletter.vue' import Header from '@/components/Header.vue' import Footer from '@/components/Footer.vue' import FollowersPopup from '@/components/popups/FollowersPopup.vue' @@ -168,6 +184,7 @@ import BrandedButton from '@/components/BrandedButton.vue' import UnauthPopup from '@/components/popups/UnauthPopup.vue' import SubInfosPopup from '@/components/popups/SubInfosPopup.vue' import unFollowWarningPopup from '@/components/popups/unFollowWarningPopup.vue' +import AddNewsletterPopup from '@/components/popups/AddNewsletter.vue' import { IBackground, backgrounds } from '@/config/backgrounds' import { createDefaultProfile, getProfile, Profile } from '@/backend/profile' @@ -177,6 +194,7 @@ import { getUserInfoNEAR } from '@/backend/near' import { ActionType, namespace as paymentProfileNamespace } from '@/store/paymentProfile' import { ISubscriptionWithProfile } from '@/store/subscriptions' import type { ISubscriptionResponse } from '@/backend/subscription' +import { IEmailSubscription, listForAuthor } from '@/backend/emails' interface IData { myProfile: Profile @@ -203,6 +221,8 @@ interface IData { showUnfollowWarning: boolean authorPaymentProfile: ISubscriptionWithProfile | undefined showChangeTier: boolean + showNewsletterPopup: boolean + newsletters: Array } export default Vue.extend({ @@ -212,6 +232,7 @@ export default Vue.extend({ Header, Footer, MutualFollowersWidget, + EmailNewsletterWidget, BrandedButton, UnauthPopup, FollowersPopup, @@ -222,6 +243,7 @@ export default Vue.extend({ SubInfosPopup, unFollowWarningPopup, ChangeTierPopup, + AddNewsletterPopup, }, middleware: `auth`, data(): IData { @@ -250,6 +272,8 @@ export default Vue.extend({ showUnfollowWarning: false, authorPaymentProfile: undefined, showChangeTier: false, + showNewsletterPopup: false, + newsletters: [], } }, watch: { @@ -297,6 +321,7 @@ export default Vue.extend({ async mounted() { // Fetch visiting profile this.getVisitingProfile() + this.fetchNewsletters() try { await this.fetchPaymentProfile({ username: this.$route.params.id }) } catch (err) { @@ -365,6 +390,16 @@ export default Vue.extend({ }) } }, + async fetchNewsletters() { + if (this.$store.state.session.id === ``) { + return + } + try { + this.newsletters = await listForAuthor(this.$route.params.id, this.$store.state.session.id) + } catch (err) { + this.$handleError(err) + } + }, async toggleFriend() { // Unauth if (this.$store.state.session.id === ``) { @@ -419,6 +454,12 @@ export default Vue.extend({ } } }, + toggleNewsletterPopup() { + this.showNewsletterPopup = !this.showNewsletterPopup + }, + refetchNewsletters() { + this.fetchNewsletters() + }, async updateFollowers() { const { followers, following } = await getFollowersAndFollowing(this.$route.params.id, true) this.followers = followers diff --git a/src/layouts/settings.vue b/src/layouts/settings.vue index 982ed1c35..863a6438b 100644 --- a/src/layouts/settings.vue +++ b/src/layouts/settings.vue @@ -26,6 +26,7 @@ @togglePopup="togglePopup" @changeLocalBGImage="changeLocalBGImage" @initProfile="initProfile" + @manageNewsletter="toggleNewsletterPopup" />