From 3e2dc0bfc494128a52b2c3e64ccaad0b29197dcb Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 3 Apr 2024 11:28:52 +0800 Subject: [PATCH 01/16] Keyboard options on the note view --- .../omnivore/omnivore/feature/notebook/NotebookView.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt index 6e381e7d8..4d2bdc3f1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt @@ -6,6 +6,7 @@ import android.content.Context import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -42,6 +43,7 @@ import app.omnivore.omnivore.core.database.entities.Highlight import app.omnivore.omnivore.feature.theme.OmnivoreTheme import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization fun notebookMD(notes: List, highlights: List): String { @@ -185,11 +187,17 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String .padding(top = paddingValues.calculateTopPadding()) .focusRequester(focusRequester) .fillMaxSize(), + keyboardOptions = KeyboardOptions.Default.copy( + autoCorrect = true, + capitalization = KeyboardCapitalization.Sentences + ), value = annotation.value, onValueChange = { annotation.value = it }, colors = TextFieldDefaults.textFieldColors( focusedTextColor = MaterialTheme.colorScheme.onSurface, unfocusedTextColor = MaterialTheme.colorScheme.onSurface, ) + + ), ) } } From 10dcb922f2f3541f3cd250bb65f93cbc8922d07f Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 1 Apr 2024 18:22:58 +0800 Subject: [PATCH 02/16] Enqueue confirmation email --- packages/api/src/jobs/send_email.ts | 26 ++++++++++++++++++++++++ packages/api/src/queue-processor.ts | 6 ++++++ packages/api/src/services/create_user.ts | 4 +--- packages/api/src/services/send_emails.ts | 20 ++++++++---------- packages/api/src/utils/createTask.ts | 22 ++++++++++++++++++++ 5 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 packages/api/src/jobs/send_email.ts diff --git a/packages/api/src/jobs/send_email.ts b/packages/api/src/jobs/send_email.ts new file mode 100644 index 000000000..c104e8eb2 --- /dev/null +++ b/packages/api/src/jobs/send_email.ts @@ -0,0 +1,26 @@ +import { env } from '../env' +import { sendWithMailJet } from '../services/send_emails' +import { sendEmail } from '../utils/sendEmail' + +export const SEND_CONFIRMATION_EMAIL_JOB = 'send-confirmation-email' + +export interface SendConfirmationEmailData { + emailAddress: string + link: string + templateData: Record +} + +export const sendConfirmationEmail = async ( + data: SendConfirmationEmailData +) => { + if (process.env.USE_MAILJET) { + return sendWithMailJet(data.emailAddress, data.link) + } + + return sendEmail({ + from: env.sender.message, + to: data.emailAddress, + templateId: env.sendgrid.confirmationTemplateId, + dynamicTemplateData: data.templateData, + }) +} diff --git a/packages/api/src/queue-processor.ts b/packages/api/src/queue-processor.ts index 32b32af47..31bca2588 100644 --- a/packages/api/src/queue-processor.ts +++ b/packages/api/src/queue-processor.ts @@ -36,6 +36,10 @@ import { import { refreshAllFeeds } from './jobs/rss/refreshAllFeeds' import { refreshFeed } from './jobs/rss/refreshFeed' import { savePageJob } from './jobs/save_page' +import { + sendConfirmationEmail, + SEND_CONFIRMATION_EMAIL_JOB, +} from './jobs/send_email' import { syncReadPositionsJob, SYNC_READ_POSITIONS_JOB_NAME, @@ -157,6 +161,8 @@ export const createWorker = (connection: ConnectionOptions) => return processYouTubeTranscript(job.data) case EXPORT_ALL_ITEMS_JOB_NAME: return exportAllItems(job.data) + case SEND_CONFIRMATION_EMAIL_JOB: + return sendConfirmationEmail(job.data) default: logger.warning(`[queue-processor] unhandled job: ${job.name}`) } diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 062dcccad..d6302ae88 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -142,9 +142,7 @@ export const createUser = async (input: { }) if (input.pendingConfirmation) { - if (!(await sendConfirmationEmail(user))) { - return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail }) - } + await sendConfirmationEmail(user) } return [user, profile] diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts index 886d22a96..a95b4a0cb 100644 --- a/packages/api/src/services/send_emails.ts +++ b/packages/api/src/services/send_emails.ts @@ -1,14 +1,15 @@ +import mailjet from 'node-mailjet' import { env } from '../env' import { generateVerificationToken } from '../utils/auth' +import { enqueueSendConfirmationEmail } from '../utils/createTask' import { logger } from '../utils/logger' import { sendEmail } from '../utils/sendEmail' -import mailjet from 'node-mailjet' export const sendConfirmationEmail = async (user: { id: string name: string email: string -}): Promise => { +}) => { // generate confirmation link const token = generateVerificationToken({ id: user.id }) const link = `${env.client.url}/auth/confirm-email/${token}` @@ -18,19 +19,14 @@ export const sendConfirmationEmail = async (user: { link, } - if (process.env.USE_MAILJET) { - return sendWithMailJet(user.email, link) - } - - return sendEmail({ - from: env.sender.message, - to: user.email, - templateId: env.sendgrid.confirmationTemplateId, - dynamicTemplateData, + await enqueueSendConfirmationEmail({ + emailAddress: user.email, + link, + templateData: dynamicTemplateData, }) } -const sendWithMailJet = async ( +export const sendWithMailJet = async ( email: string, link: string ): Promise => { diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index b69b8e8d3..8b6bb3514 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -35,6 +35,10 @@ import { REFRESH_ALL_FEEDS_JOB_NAME, REFRESH_FEED_JOB_NAME, } from '../jobs/rss/refreshAllFeeds' +import { + SendConfirmationEmailData, + SEND_CONFIRMATION_EMAIL_JOB, +} from '../jobs/send_email' import { SYNC_READ_POSITIONS_JOB_NAME } from '../jobs/sync_read_positions' import { TriggerRuleJobData, TRIGGER_RULE_JOB_NAME } from '../jobs/trigger_rule' import { @@ -69,6 +73,7 @@ export const getJobPriority = (jobName: string): number => { case UPDATE_LABELS_JOB: case UPDATE_HIGHLIGHT_JOB: case SYNC_READ_POSITIONS_JOB_NAME: + case SEND_CONFIRMATION_EMAIL_JOB: return 1 case TRIGGER_RULE_JOB_NAME: case CALL_WEBHOOK_JOB_NAME: @@ -839,4 +844,21 @@ export const enqueueExportItem = async (jobData: ExportItemJobData) => { }) } +export const enqueueSendConfirmationEmail = async ( + jobData: SendConfirmationEmailData +) => { + const queue = await getBackendQueue() + if (!queue) { + return undefined + } + + return queue.add(SEND_CONFIRMATION_EMAIL_JOB, jobData, { + attempts: 1, // only try once + priority: getJobPriority(SEND_CONFIRMATION_EMAIL_JOB), + jobId: `${SEND_CONFIRMATION_EMAIL_JOB}_${jobData.emailAddress}_${JOB_VERSION}`, // deduplication + removeOnComplete: true, + removeOnFail: true, + }) +} + export default createHttpTaskWithToken From c37b31dcb671b001030de7e1732dca76e08ccd07 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 2 Apr 2024 10:21:57 +0800 Subject: [PATCH 03/16] fix tests --- packages/api/src/services/send_emails.ts | 4 +- packages/api/src/utils/createTask.ts | 2 +- packages/api/test/routers/auth.test.ts | 47 +++++-------------- .../api/test/services/create_user.test.ts | 32 +++++-------- 4 files changed, 26 insertions(+), 59 deletions(-) diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts index a95b4a0cb..3ccb56132 100644 --- a/packages/api/src/services/send_emails.ts +++ b/packages/api/src/services/send_emails.ts @@ -1,7 +1,7 @@ import mailjet from 'node-mailjet' import { env } from '../env' import { generateVerificationToken } from '../utils/auth' -import { enqueueSendConfirmationEmail } from '../utils/createTask' +import { enqueueConfirmationEmail } from '../utils/createTask' import { logger } from '../utils/logger' import { sendEmail } from '../utils/sendEmail' @@ -19,7 +19,7 @@ export const sendConfirmationEmail = async (user: { link, } - await enqueueSendConfirmationEmail({ + await enqueueConfirmationEmail({ emailAddress: user.email, link, templateData: dynamicTemplateData, diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 8b6bb3514..b42884386 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -844,7 +844,7 @@ export const enqueueExportItem = async (jobData: ExportItemJobData) => { }) } -export const enqueueSendConfirmationEmail = async ( +export const enqueueConfirmationEmail = async ( jobData: SendConfirmationEmailData ) => { const queue = await getBackendQueue() diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index 58bb48ab6..d00c5f3ef 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -1,11 +1,14 @@ import { MailDataRequired } from '@sendgrid/helpers/classes/mail' +import { Job } from 'bullmq' import chai, { expect } from 'chai' import sinon from 'sinon' import sinonChai from 'sinon-chai' import supertest from 'supertest' import { StatusType, User } from '../../src/entity/user' +import { SendConfirmationEmailData } from '../../src/jobs/send_email' import { getRepository } from '../../src/repository' import { userRepository } from '../../src/repository/user' +import { isValidSignupRequest } from '../../src/routers/auth/auth_router' import { AuthProvider } from '../../src/routers/auth/auth_types' import { createPendingUserToken } from '../../src/routers/auth/jwt_helpers' import { searchLibraryItems } from '../../src/services/library_item' @@ -15,10 +18,10 @@ import { generateVerificationToken, hashPassword, } from '../../src/utils/auth' +import * as createTask from '../../src/utils/createTask' import * as util from '../../src/utils/sendEmail' import { createTestUser } from '../db' import { generateFakeUuid, request } from '../util' -import { isValidSignupRequest } from '../../src/routers/auth/auth_router' chai.use(sinonChai) @@ -47,8 +50,6 @@ describe('auth router', () => { let name: string context('when inputs are valid and user not exists', () => { - let fake: (msg: MailDataRequired) => Promise - before(() => { password = validPassword username = 'Some_username' @@ -62,14 +63,6 @@ describe('auth router', () => { }) context('when confirmation email sent', () => { - beforeEach(() => { - fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true)) - }) - - afterEach(() => { - sinon.restore() - }) - it('redirects to verify email', async () => { const res = await signupRequest( email, @@ -90,28 +83,6 @@ describe('auth router', () => { expect(user?.name).to.eql(name) }) }) - - context('when confirmation email not sent', () => { - before(() => { - fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(false)) - }) - - after(() => { - sinon.restore() - }) - - it('redirects to sign up page with error code INVALID_EMAIL', async () => { - const res = await signupRequest( - email, - password, - name, - username - ).expect(302) - expect(res.header.location).to.endWith( - '/email-signup?errorCodes=INVALID_EMAIL' - ) - }) - }) }) context('when user exists', () => { @@ -214,10 +185,16 @@ describe('auth router', () => { }) context('when user is not confirmed', async () => { - let fake: (msg: MailDataRequired) => Promise + let fake: ( + jobData: SendConfirmationEmailData + ) => Promise | undefined> beforeEach(async () => { - fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true)) + fake = sinon.replace( + createTask, + 'enqueueConfirmationEmail', + sinon.fake() + ) await updateUser(user.id, { status: StatusType.Pending }) email = user.email password = correctPassword diff --git a/packages/api/test/services/create_user.test.ts b/packages/api/test/services/create_user.test.ts index 4d4b21c26..23a210b8e 100644 --- a/packages/api/test/services/create_user.test.ts +++ b/packages/api/test/services/create_user.test.ts @@ -1,14 +1,15 @@ -import { MailDataRequired } from '@sendgrid/helpers/classes/mail' +import { Job } from 'bullmq' import chai, { expect } from 'chai' import 'mocha' import sinon from 'sinon' import sinonChai from 'sinon-chai' import { Filter } from '../../src/entity/filter' import { StatusType, User } from '../../src/entity/user' +import { SendConfirmationEmailData } from '../../src/jobs/send_email' import { authTrx, getRepository } from '../../src/repository' import { findProfile } from '../../src/services/profile' import { deleteUser } from '../../src/services/user' -import * as util from '../../src/utils/sendEmail' +import * as createTask from '../../src/utils/createTask' import { createTestUser, createUserWithoutProfile, @@ -95,11 +96,17 @@ describe('create user', () => { context('create a user with pending confirmation', () => { const name = 'pendingUser' - let fake: (msg: MailDataRequired) => Promise + let fake: ( + jobData: SendConfirmationEmailData + ) => Promise | undefined> context('when email sends successfully', () => { beforeEach(() => { - fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true)) + fake = sinon.replace( + createTask, + 'enqueueConfirmationEmail', + sinon.fake() + ) }) afterEach(async () => { @@ -121,22 +128,5 @@ describe('create user', () => { expect(fake).to.have.been.calledOnce }) }) - - context('when failed to send email', () => { - before(() => { - fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(false)) - }) - - after(async () => { - sinon.restore() - const user = await getRepository(User).findOneBy({ name }) - await deleteUser(user!.id) - }) - - it('rejects with error', async () => { - return expect(createTestUser(name, undefined, undefined, true)).to.be - .rejected - }) - }) }) }) From 8af2bb076c8956dea91d856714ecc19bd09e08c8 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 12:16:28 +0800 Subject: [PATCH 04/16] rename send mail functions --- packages/api/src/jobs/send_email.ts | 37 ++++++++++------ packages/api/src/queue-processor.ts | 9 ++-- packages/api/src/resolvers/user/index.ts | 4 +- packages/api/src/routers/auth/auth_router.ts | 4 +- .../api/src/routers/auth/mobile/sign_in.ts | 4 +- packages/api/src/services/create_user.ts | 4 +- packages/api/src/services/send_emails.ts | 43 ++++++++----------- packages/api/src/utils/createTask.ts | 18 +++----- .../api/test/services/create_user.test.ts | 4 +- 9 files changed, 61 insertions(+), 66 deletions(-) diff --git a/packages/api/src/jobs/send_email.ts b/packages/api/src/jobs/send_email.ts index c104e8eb2..8831fd970 100644 --- a/packages/api/src/jobs/send_email.ts +++ b/packages/api/src/jobs/send_email.ts @@ -1,26 +1,37 @@ import { env } from '../env' import { sendWithMailJet } from '../services/send_emails' +import { Merge } from '../util' +import { logger } from '../utils/logger' import { sendEmail } from '../utils/sendEmail' -export const SEND_CONFIRMATION_EMAIL_JOB = 'send-confirmation-email' +export const SEND_EMAIL_JOB = 'send-email' -export interface SendConfirmationEmailData { - emailAddress: string - link: string - templateData: Record -} +type ContentType = { html: string } | { text: string } | { templateId: string } +export type SendEmailJobData = Merge< + { + emailAddress: string + subject?: string + html?: string + text?: string + templateId?: string + dynamicTemplateData?: Record + }, + ContentType +> -export const sendConfirmationEmail = async ( - data: SendConfirmationEmailData -) => { - if (process.env.USE_MAILJET) { - return sendWithMailJet(data.emailAddress, data.link) +export const sendEmailJob = async (data: SendEmailJobData) => { + if (process.env.USE_MAILJET && data.dynamicTemplateData) { + return sendWithMailJet(data.emailAddress, data.dynamicTemplateData.link) + } + + if (!data.html && !data.text && !data.templateId) { + logger.error('no email content provided', data) + return false } return sendEmail({ + ...data, from: env.sender.message, to: data.emailAddress, - templateId: env.sendgrid.confirmationTemplateId, - dynamicTemplateData: data.templateData, }) } diff --git a/packages/api/src/queue-processor.ts b/packages/api/src/queue-processor.ts index 31bca2588..aab69d3dc 100644 --- a/packages/api/src/queue-processor.ts +++ b/packages/api/src/queue-processor.ts @@ -36,10 +36,7 @@ import { import { refreshAllFeeds } from './jobs/rss/refreshAllFeeds' import { refreshFeed } from './jobs/rss/refreshFeed' import { savePageJob } from './jobs/save_page' -import { - sendConfirmationEmail, - SEND_CONFIRMATION_EMAIL_JOB, -} from './jobs/send_email' +import { sendEmailJob, SEND_EMAIL_JOB } from './jobs/send_email' import { syncReadPositionsJob, SYNC_READ_POSITIONS_JOB_NAME, @@ -161,8 +158,8 @@ export const createWorker = (connection: ConnectionOptions) => return processYouTubeTranscript(job.data) case EXPORT_ALL_ITEMS_JOB_NAME: return exportAllItems(job.data) - case SEND_CONFIRMATION_EMAIL_JOB: - return sendConfirmationEmail(job.data) + case SEND_EMAIL_JOB: + return sendEmailJob(job.data) default: logger.warning(`[queue-processor] unhandled job: ${job.name}`) } diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index 8303d41ea..5f794aff0 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -41,7 +41,7 @@ import { } from '../../generated/graphql' import { userRepository } from '../../repository/user' import { createUser } from '../../services/create_user' -import { sendVerificationEmail } from '../../services/send_emails' +import { sendAccountChangeEmail } from '../../services/send_emails' import { softDeleteUser } from '../../services/user' import { userDataToUser } from '../../utils/helpers' import { validateUsername } from '../../utils/usernamePolicy' @@ -358,7 +358,7 @@ export const updateEmailResolver = authorized< return { email } } - const result = await sendVerificationEmail({ + const result = await sendAccountChangeEmail({ id: user.id, name: user.name, email, diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index ec18ffcbb..86374a3d1 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -23,7 +23,7 @@ import { userRepository } from '../../repository/user' import { isErrorWithCode } from '../../resolvers' import { createUser } from '../../services/create_user' import { - sendConfirmationEmail, + sendNewAccountVerificationEmail, sendPasswordResetEmail, } from '../../services/send_emails' import { analytics } from '../../utils/analytics' @@ -440,7 +440,7 @@ export function authRouter() { } if (user.status === StatusType.Pending && user.email) { - await sendConfirmationEmail({ + await sendNewAccountVerificationEmail({ id: user.id, email: user.email, name: user.name, diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index 4af80f0a6..ee4c7778a 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { StatusType } from '../../../entity/user' import { userRepository } from '../../../repository/user' -import { sendConfirmationEmail } from '../../../services/send_emails' +import { sendNewAccountVerificationEmail } from '../../../services/send_emails' import { comparePassword } from '../../../utils/auth' import { logger } from '../../../utils/logger' import { decodeAppleToken } from '../apple_auth' @@ -56,7 +56,7 @@ export async function createMobileEmailSignInResponse( } if (user.status === StatusType.Pending && user.email) { - await sendConfirmationEmail({ + await sendNewAccountVerificationEmail({ id: user.id, email: user.email, name: user.name, diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index d6302ae88..4bacfd9e6 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -16,7 +16,7 @@ import { IntercomClient } from '../utils/intercom' import { logger } from '../utils/logger' import { validateUsername } from '../utils/usernamePolicy' import { addPopularReadsForNewUser } from './popular_reads' -import { sendConfirmationEmail } from './send_emails' +import { sendNewAccountVerificationEmail } from './send_emails' export const MAX_RECORDS_LIMIT = 1000 @@ -142,7 +142,7 @@ export const createUser = async (input: { }) if (input.pendingConfirmation) { - await sendConfirmationEmail(user) + await sendNewAccountVerificationEmail(user) } return [user, profile] diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts index 3ccb56132..82e24390e 100644 --- a/packages/api/src/services/send_emails.ts +++ b/packages/api/src/services/send_emails.ts @@ -1,15 +1,14 @@ import mailjet from 'node-mailjet' import { env } from '../env' import { generateVerificationToken } from '../utils/auth' -import { enqueueConfirmationEmail } from '../utils/createTask' +import { enqueueSendEmail } from '../utils/createTask' import { logger } from '../utils/logger' -import { sendEmail } from '../utils/sendEmail' -export const sendConfirmationEmail = async (user: { +export const sendNewAccountVerificationEmail = async (user: { id: string name: string email: string -}) => { +}): Promise => { // generate confirmation link const token = generateVerificationToken({ id: user.id }) const link = `${env.client.url}/auth/confirm-email/${token}` @@ -19,11 +18,13 @@ export const sendConfirmationEmail = async (user: { link, } - await enqueueConfirmationEmail({ + const result = await enqueueSendEmail({ emailAddress: user.email, - link, - templateData: dynamicTemplateData, + dynamicTemplateData: dynamicTemplateData, + templateId: env.sendgrid.confirmationTemplateId, }) + + return !!result } export const sendWithMailJet = async ( @@ -64,7 +65,7 @@ export const sendWithMailJet = async ( return true } -export const sendVerificationEmail = async (user: { +export const sendAccountChangeEmail = async (user: { id: string name: string email: string @@ -78,16 +79,13 @@ export const sendVerificationEmail = async (user: { link, } - if (process.env.USE_MAILJET) { - return sendWithMailJet(user.email, link) - } - - return sendEmail({ - from: env.sender.message, - to: user.email, + const result = await enqueueSendEmail({ + emailAddress: user.email, + dynamicTemplateData: dynamicTemplateData, templateId: env.sendgrid.verificationTemplateId, - dynamicTemplateData, }) + + return !!result } export const sendPasswordResetEmail = async (user: { @@ -104,14 +102,11 @@ export const sendPasswordResetEmail = async (user: { link, } - if (process.env.USE_MAILJET) { - return sendWithMailJet(user.email, link) - } - - return sendEmail({ - from: env.sender.message, - to: user.email, + const result = await enqueueSendEmail({ + emailAddress: user.email, + dynamicTemplateData: dynamicTemplateData, templateId: env.sendgrid.resetPasswordTemplateId, - dynamicTemplateData, }) + + return !!result } diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index b42884386..c2ce23fa9 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -35,10 +35,7 @@ import { REFRESH_ALL_FEEDS_JOB_NAME, REFRESH_FEED_JOB_NAME, } from '../jobs/rss/refreshAllFeeds' -import { - SendConfirmationEmailData, - SEND_CONFIRMATION_EMAIL_JOB, -} from '../jobs/send_email' +import { SendEmailJobData, SEND_EMAIL_JOB } from '../jobs/send_email' import { SYNC_READ_POSITIONS_JOB_NAME } from '../jobs/sync_read_positions' import { TriggerRuleJobData, TRIGGER_RULE_JOB_NAME } from '../jobs/trigger_rule' import { @@ -73,7 +70,7 @@ export const getJobPriority = (jobName: string): number => { case UPDATE_LABELS_JOB: case UPDATE_HIGHLIGHT_JOB: case SYNC_READ_POSITIONS_JOB_NAME: - case SEND_CONFIRMATION_EMAIL_JOB: + case SEND_EMAIL_JOB: return 1 case TRIGGER_RULE_JOB_NAME: case CALL_WEBHOOK_JOB_NAME: @@ -844,20 +841,15 @@ export const enqueueExportItem = async (jobData: ExportItemJobData) => { }) } -export const enqueueConfirmationEmail = async ( - jobData: SendConfirmationEmailData -) => { +export const enqueueSendEmail = async (jobData: SendEmailJobData) => { const queue = await getBackendQueue() if (!queue) { return undefined } - return queue.add(SEND_CONFIRMATION_EMAIL_JOB, jobData, { + return queue.add(SEND_EMAIL_JOB, jobData, { attempts: 1, // only try once - priority: getJobPriority(SEND_CONFIRMATION_EMAIL_JOB), - jobId: `${SEND_CONFIRMATION_EMAIL_JOB}_${jobData.emailAddress}_${JOB_VERSION}`, // deduplication - removeOnComplete: true, - removeOnFail: true, + priority: getJobPriority(SEND_EMAIL_JOB), }) } diff --git a/packages/api/test/services/create_user.test.ts b/packages/api/test/services/create_user.test.ts index 23a210b8e..2c46acabf 100644 --- a/packages/api/test/services/create_user.test.ts +++ b/packages/api/test/services/create_user.test.ts @@ -5,7 +5,7 @@ import sinon from 'sinon' import sinonChai from 'sinon-chai' import { Filter } from '../../src/entity/filter' import { StatusType, User } from '../../src/entity/user' -import { SendConfirmationEmailData } from '../../src/jobs/send_email' +import { SendTemplatedEmailData } from '../../src/jobs/send_email' import { authTrx, getRepository } from '../../src/repository' import { findProfile } from '../../src/services/profile' import { deleteUser } from '../../src/services/user' @@ -97,7 +97,7 @@ describe('create user', () => { context('create a user with pending confirmation', () => { const name = 'pendingUser' let fake: ( - jobData: SendConfirmationEmailData + jobData: SendTemplatedEmailData ) => Promise | undefined> context('when email sends successfully', () => { From af140cc8b4c9ead46b0a542acb65897b2a92dea0 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 12:17:22 +0800 Subject: [PATCH 05/16] remove user name from email template --- packages/api/src/services/send_emails.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts index 82e24390e..70dfb019f 100644 --- a/packages/api/src/services/send_emails.ts +++ b/packages/api/src/services/send_emails.ts @@ -14,7 +14,6 @@ export const sendNewAccountVerificationEmail = async (user: { const link = `${env.client.url}/auth/confirm-email/${token}` // send email const dynamicTemplateData = { - name: user.name, link, } @@ -75,7 +74,6 @@ export const sendAccountChangeEmail = async (user: { const link = `${env.client.url}/auth/reset-password/${token}` // send email const dynamicTemplateData = { - name: user.name, link, } @@ -98,7 +96,6 @@ export const sendPasswordResetEmail = async (user: { const link = `${env.client.url}/auth/reset-password/${token}` // send email const dynamicTemplateData = { - name: user.name, link, } From 46e3936a85fc93d1284c8328ed2b13b44f97275b Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 17:13:10 +0800 Subject: [PATCH 06/16] resolve conflicts after rebasing --- packages/api/test/routers/auth.test.ts | 40 +++++++++---------- .../api/test/services/create_user.test.ts | 10 ++--- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index d00c5f3ef..f09441d85 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -5,7 +5,7 @@ import sinon from 'sinon' import sinonChai from 'sinon-chai' import supertest from 'supertest' import { StatusType, User } from '../../src/entity/user' -import { SendConfirmationEmailData } from '../../src/jobs/send_email' +import { SendEmailJobData } from '../../src/jobs/send_email' import { getRepository } from '../../src/repository' import { userRepository } from '../../src/repository/user' import { isValidSignupRequest } from '../../src/routers/auth/auth_router' @@ -184,17 +184,13 @@ describe('auth router', () => { }) }) - context('when user is not confirmed', async () => { + context('when user is not confirmed', () => { let fake: ( - jobData: SendConfirmationEmailData + jobData: SendEmailJobData ) => Promise | undefined> beforeEach(async () => { - fake = sinon.replace( - createTask, - 'enqueueConfirmationEmail', - sinon.fake() - ) + fake = sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) await updateUser(user.id, { status: StatusType.Pending }) email = user.email password = correctPassword @@ -231,7 +227,7 @@ describe('auth router', () => { }) }) - context('when user has no password stored in db', async () => { + context('when user has no password stored in db', () => { before(async () => { await updateUser(user.id, { password: '' }) email = user.email @@ -476,7 +472,7 @@ describe('auth router', () => { }) context('when token is valid', () => { - before(async () => { + before(() => { token = generateVerificationToken({ id: user.id }) }) @@ -494,8 +490,8 @@ describe('auth router', () => { const updatedUser = await getRepository(User).findOneBy({ id: user?.id, }) - expect(await comparePassword(password, updatedUser?.password!)).to.be - .true + const newPassword = updatedUser?.password || '' + expect(await comparePassword(password, newPassword)).to.be.true }) }) @@ -557,12 +553,12 @@ describe('auth router', () => { } context('when inputs are valid and user not exists', () => { - let name = 'test_user' - let username = 'test_user' - let sourceUserId = 'test_source_user_id' - let email = 'test_user@omnivore.app' - let bio = 'test_bio' - let provider: AuthProvider = 'EMAIL' + const name = 'test_user' + const username = 'test_user' + const sourceUserId = 'test_source_user_id' + const email = 'test_user@omnivore.app' + const bio = 'test_bio' + const provider: AuthProvider = 'EMAIL' afterEach(async () => { const user = await userRepository.findOneByOrFail({ name }) @@ -618,7 +614,7 @@ describe('auth router', () => { }) describe('isValidSignupRequest', () => { - it('returns true for normal looking requests', async () => { + it('returns true for normal looking requests', () => { const result = isValidSignupRequest({ email: 'email@omnivore.app', password: 'superDuperPassword', @@ -627,7 +623,7 @@ describe('isValidSignupRequest', () => { }) expect(result).to.be.true }) - it('returns false for requests w/missing info', async () => { + it('returns false for requests w/missing info', () => { let result = isValidSignupRequest({ password: 'superDuperPassword', name: "The User's Name", @@ -657,8 +653,8 @@ describe('isValidSignupRequest', () => { expect(result).to.be.false }) - it('returns false for requests w/malicious info', async () => { - let result = isValidSignupRequest({ + it('returns false for requests w/malicious info', () => { + const result = isValidSignupRequest({ password: 'superDuperPassword', name: "You've won a cake sign up here: https://foo.bar", username: 'foouser', diff --git a/packages/api/test/services/create_user.test.ts b/packages/api/test/services/create_user.test.ts index 2c46acabf..039aa05e4 100644 --- a/packages/api/test/services/create_user.test.ts +++ b/packages/api/test/services/create_user.test.ts @@ -5,7 +5,7 @@ import sinon from 'sinon' import sinonChai from 'sinon-chai' import { Filter } from '../../src/entity/filter' import { StatusType, User } from '../../src/entity/user' -import { SendTemplatedEmailData } from '../../src/jobs/send_email' +import { SendEmailJobData } from '../../src/jobs/send_email' import { authTrx, getRepository } from '../../src/repository' import { findProfile } from '../../src/services/profile' import { deleteUser } from '../../src/services/user' @@ -97,16 +97,12 @@ describe('create user', () => { context('create a user with pending confirmation', () => { const name = 'pendingUser' let fake: ( - jobData: SendTemplatedEmailData + jobData: SendEmailJobData ) => Promise | undefined> context('when email sends successfully', () => { beforeEach(() => { - fake = sinon.replace( - createTask, - 'enqueueConfirmationEmail', - sinon.fake() - ) + fake = sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) }) afterEach(async () => { From f8a6887d092a73cb45593001d2d697974f3cdd98 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 17:26:38 +0800 Subject: [PATCH 07/16] show failed_at timestamp if a feed failed to fresh --- .../queries/useGetSubscriptionsQuery.tsx | 2 ++ packages/web/pages/settings/feeds/index.tsx | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx index b05d64eb9..58ec302dd 100644 --- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx @@ -30,6 +30,7 @@ export type Subscription = { updatedAt: string lastFetchedAt?: string mostRecentItemDate?: string + failedAt?: string fetchContentType?: FetchContentType } @@ -74,6 +75,7 @@ export function useGetSubscriptionsQuery( lastFetchedAt fetchContentType mostRecentItemDate + failedAt } } ... on SubscriptionsError { diff --git a/packages/web/pages/settings/feeds/index.tsx b/packages/web/pages/settings/feeds/index.tsx index 37bc5dae1..fcff2c823 100644 --- a/packages/web/pages/settings/feeds/index.tsx +++ b/packages/web/pages/settings/feeds/index.tsx @@ -229,10 +229,17 @@ export default function Rss(): JSX.Element { }} > {`URL: ${subscription.url}`} - {`Last refreshed: ${ - subscription.lastFetchedAt - ? formattedDateTime(subscription.lastFetchedAt) - : 'Never' + {/* show failed timestamp instead of last refreshed timestamp if the feed failed to refresh */} + {`${ + subscription.failedAt + ? `Failed to refresh: ${formattedDateTime( + subscription.failedAt + )}` + : `Last refreshed: ${ + subscription.lastFetchedAt + ? formattedDateTime(subscription.lastFetchedAt) + : 'Never' + }` }`} {subscription.mostRecentItemDate && From 5446ca80dc47ddb9d7a005eccac205fae7e7de86 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 17:53:18 +0800 Subject: [PATCH 08/16] fix tests --- packages/api/test/routers/auth.test.ts | 27 +++----------------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index f09441d85..92ded5e51 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -1,4 +1,3 @@ -import { MailDataRequired } from '@sendgrid/helpers/classes/mail' import { Job } from 'bullmq' import chai, { expect } from 'chai' import sinon from 'sinon' @@ -19,7 +18,6 @@ import { hashPassword, } from '../../src/utils/auth' import * as createTask from '../../src/utils/createTask' -import * as util from '../../src/utils/sendEmail' import { createTestUser } from '../db' import { generateFakeUuid, request } from '../util' @@ -270,7 +268,7 @@ describe('auth router', () => { let token: string before(async () => { - sinon.replace(util, 'sendEmail', sinon.fake.resolves(true)) + sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) user = await createTestUser('pendingUser', undefined, 'password', true) }) @@ -368,15 +366,13 @@ describe('auth router', () => { }) context('when email is verified', () => { - let fake: (msg: MailDataRequired) => Promise - before(async () => { await updateUser(user.id, { status: StatusType.Active }) }) context('when reset password email sent', () => { before(() => { - fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true)) + sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) }) after(() => { @@ -391,11 +387,7 @@ describe('auth router', () => { context('when reset password email not sent', () => { before(() => { - fake = sinon.replace( - util, - 'sendEmail', - sinon.fake.resolves(false) - ) + sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) }) after(() => { @@ -434,19 +426,6 @@ describe('auth router', () => { }) }) }) - - context('when email is empty', () => { - before(() => { - email = '' - }) - - it('redirects to forgot-password page with error code INVALID_EMAIL', async () => { - const res = await emailResetPasswordReq(email).expect(302) - expect(res.header.location).to.endWith( - '/forgot-password?errorCodes=INVALID_EMAIL' - ) - }) - }) }) describe('reset-password', () => { From 65ba80b5a369de1546a1a040c2a42eda20dc901b Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 18:02:52 +0800 Subject: [PATCH 09/16] fix tests --- packages/api/test/routers/auth.test.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index 92ded5e51..f1f6c5da6 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -1,4 +1,4 @@ -import { Job } from 'bullmq' +import { Job, Queue } from 'bullmq' import chai, { expect } from 'chai' import sinon from 'sinon' import sinonChai from 'sinon-chai' @@ -372,7 +372,11 @@ describe('auth router', () => { context('when reset password email sent', () => { before(() => { - sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) + sinon.replace( + createTask, + 'enqueueSendEmail', + sinon.fake.resolves(new Job(new Queue('test'), 'test', 'test')) + ) }) after(() => { @@ -426,6 +430,19 @@ describe('auth router', () => { }) }) }) + + context('when email is empty', () => { + before(() => { + email = '' + }) + + it('redirects to forgot-password page with error code INVALID_EMAIL', async () => { + const res = await emailResetPasswordReq(email).expect(302) + expect(res.header.location).to.endWith( + '/forgot-password?errorCodes=INVALID_EMAIL' + ) + }) + }) }) describe('reset-password', () => { From 7abb396032061ad08993d783ac8a5cb95ea95a70 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 5 Apr 2024 22:53:03 +0800 Subject: [PATCH 10/16] remove stub --- packages/api/src/services/create_user.ts | 4 +- packages/api/test/routers/auth.test.ts | 51 +------------------ .../api/test/services/create_user.test.ts | 24 +-------- 3 files changed, 5 insertions(+), 74 deletions(-) diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 4bacfd9e6..c71da06cd 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -142,7 +142,9 @@ export const createUser = async (input: { }) if (input.pendingConfirmation) { - await sendNewAccountVerificationEmail(user) + if (!(await sendNewAccountVerificationEmail(user))) { + return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail }) + } } return [user, profile] diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index f1f6c5da6..8bfd3e5e4 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -1,10 +1,6 @@ -import { Job, Queue } from 'bullmq' -import chai, { expect } from 'chai' -import sinon from 'sinon' -import sinonChai from 'sinon-chai' +import { expect } from 'chai' import supertest from 'supertest' import { StatusType, User } from '../../src/entity/user' -import { SendEmailJobData } from '../../src/jobs/send_email' import { getRepository } from '../../src/repository' import { userRepository } from '../../src/repository/user' import { isValidSignupRequest } from '../../src/routers/auth/auth_router' @@ -17,12 +13,9 @@ import { generateVerificationToken, hashPassword, } from '../../src/utils/auth' -import * as createTask from '../../src/utils/createTask' import { createTestUser } from '../db' import { generateFakeUuid, request } from '../util' -chai.use(sinonChai) - describe('auth router', () => { const route = '/api/auth' @@ -183,12 +176,7 @@ describe('auth router', () => { }) context('when user is not confirmed', () => { - let fake: ( - jobData: SendEmailJobData - ) => Promise | undefined> - beforeEach(async () => { - fake = sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) await updateUser(user.id, { status: StatusType.Pending }) email = user.email password = correctPassword @@ -196,7 +184,6 @@ describe('auth router', () => { afterEach(async () => { await updateUser(user.id, { status: StatusType.Active }) - sinon.restore() }) it('redirects with error code PendingVerification', async () => { @@ -205,11 +192,6 @@ describe('auth router', () => { '/email-login?errorCodes=PENDING_VERIFICATION' ) }) - - it('sends a verification email', async () => { - await loginRequest(email, password).expect(302) - expect(fake).to.have.been.calledOnce - }) }) context('when user not exists', () => { @@ -268,12 +250,10 @@ describe('auth router', () => { let token: string before(async () => { - sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) user = await createTestUser('pendingUser', undefined, 'password', true) }) after(async () => { - sinon.restore() await deleteUser(user.id) }) @@ -371,40 +351,11 @@ describe('auth router', () => { }) context('when reset password email sent', () => { - before(() => { - sinon.replace( - createTask, - 'enqueueSendEmail', - sinon.fake.resolves(new Job(new Queue('test'), 'test', 'test')) - ) - }) - - after(() => { - sinon.restore() - }) - it('redirects to forgot-password page with success message', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith('/auth/reset-sent') }) }) - - context('when reset password email not sent', () => { - before(() => { - sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) - }) - - after(() => { - sinon.restore() - }) - - it('redirects to sign up page with error code INVALID_EMAIL', async () => { - const res = await emailResetPasswordReq(email).expect(302) - expect(res.header.location).to.endWith( - '/forgot-password?errorCodes=INVALID_EMAIL' - ) - }) - }) }) context('when email is not verified', () => { diff --git a/packages/api/test/services/create_user.test.ts b/packages/api/test/services/create_user.test.ts index 039aa05e4..3449b5f9d 100644 --- a/packages/api/test/services/create_user.test.ts +++ b/packages/api/test/services/create_user.test.ts @@ -1,23 +1,15 @@ -import { Job } from 'bullmq' -import chai, { expect } from 'chai' -import 'mocha' -import sinon from 'sinon' -import sinonChai from 'sinon-chai' +import { expect } from 'chai' import { Filter } from '../../src/entity/filter' import { StatusType, User } from '../../src/entity/user' -import { SendEmailJobData } from '../../src/jobs/send_email' import { authTrx, getRepository } from '../../src/repository' import { findProfile } from '../../src/services/profile' import { deleteUser } from '../../src/services/user' -import * as createTask from '../../src/utils/createTask' import { createTestUser, createUserWithoutProfile, deleteFiltersFromUser, } from '../db' -chai.use(sinonChai) - describe('create user', () => { context('creates a user through manual sign up', () => { it('adds the default filters to the user', async () => { @@ -96,17 +88,9 @@ describe('create user', () => { context('create a user with pending confirmation', () => { const name = 'pendingUser' - let fake: ( - jobData: SendEmailJobData - ) => Promise | undefined> context('when email sends successfully', () => { - beforeEach(() => { - fake = sinon.replace(createTask, 'enqueueSendEmail', sinon.fake()) - }) - afterEach(async () => { - sinon.restore() const user = await getRepository(User).findOneBy({ name }) await deleteUser(user!.id) }) @@ -117,12 +101,6 @@ describe('create user', () => { expect(user.status).to.eql(StatusType.Pending) expect(user.name).to.eql(name) }) - - it('sends an email to the user', async () => { - await createTestUser(name, undefined, undefined, true) - - expect(fake).to.have.been.calledOnce - }) }) }) }) From 8f5cd7dba8c34380019714ead1cfd99f882ecd1c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 8 Apr 2024 16:01:48 +0800 Subject: [PATCH 11/16] Fix build issue --- .../app/omnivore/omnivore/feature/notebook/NotebookView.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt index 4d2bdc3f1..27f9147aa 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/notebook/NotebookView.kt @@ -181,7 +181,6 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String ) } ) { paddingValues -> - TextField( modifier = Modifier .padding(top = paddingValues.calculateTopPadding()) @@ -196,8 +195,6 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String focusedTextColor = MaterialTheme.colorScheme.onSurface, unfocusedTextColor = MaterialTheme.colorScheme.onSurface, ) - - ), ) } } From 33e8e4b75a98efb779e254a781c257c83c06a908 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 8 Apr 2024 16:02:03 +0800 Subject: [PATCH 12/16] Bump database version --- .../app/omnivore/omnivore/core/database/OmnivoreDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt index cb8db0c08..7739afffb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt @@ -27,7 +27,7 @@ import app.omnivore.omnivore.core.database.entities.ViewerDao HighlightChange::class, SavedItemAndSavedItemLabelCrossRef::class, SavedItemAndHighlightCrossRef::class], - version = 24, + version = 26, exportSchema = true ) abstract class OmnivoreDatabase : RoomDatabase() { From 60c78158c40450d70937cecda6227c2ecde0da2a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 8 Apr 2024 16:02:20 +0800 Subject: [PATCH 13/16] Bump android version --- android/Omnivore/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/build.gradle.kts b/android/Omnivore/app/build.gradle.kts index b77565308..b07b7a768 100644 --- a/android/Omnivore/app/build.gradle.kts +++ b/android/Omnivore/app/build.gradle.kts @@ -27,8 +27,8 @@ android { applicationId = "app.omnivore.omnivore" minSdk = 26 targetSdk = 34 - versionCode = 194001 - versionName = "0.195.0" + versionCode = 200001 + versionName = "0.200.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 5dfbd7f2386d9bb4cbe64da65ee0e5b1f78a4101 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 8 Apr 2024 16:32:43 +0800 Subject: [PATCH 14/16] failed timestamp text is in red color --- packages/web/pages/settings/feeds/index.tsx | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/web/pages/settings/feeds/index.tsx b/packages/web/pages/settings/feeds/index.tsx index fcff2c823..6084ac145 100644 --- a/packages/web/pages/settings/feeds/index.tsx +++ b/packages/web/pages/settings/feeds/index.tsx @@ -230,17 +230,19 @@ export default function Rss(): JSX.Element { > {`URL: ${subscription.url}`} {/* show failed timestamp instead of last refreshed timestamp if the feed failed to refresh */} - {`${ - subscription.failedAt - ? `Failed to refresh: ${formattedDateTime( - subscription.failedAt - )}` - : `Last refreshed: ${ - subscription.lastFetchedAt - ? formattedDateTime(subscription.lastFetchedAt) - : 'Never' - }` - }`} + {subscription.failedAt ? ( + {`Failed to refresh: ${formattedDateTime( + subscription.failedAt + )}`} + ) : ( + {`Last refreshed: ${ + subscription.lastFetchedAt + ? formattedDateTime(subscription.lastFetchedAt) + : 'Never' + }`} + )} {subscription.mostRecentItemDate && `Most recent item: ${formattedDateTime( From f3cb55f8c6e50a129e59330ea1d3403be322ec12 Mon Sep 17 00:00:00 2001 From: Stefano Sansone Date: Mon, 8 Apr 2024 12:05:43 +0200 Subject: [PATCH 15/16] allow room destructive migration --- android/Omnivore/app/build.gradle.kts | 4 ++-- .../src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/Omnivore/app/build.gradle.kts b/android/Omnivore/app/build.gradle.kts index b07b7a768..fbd681889 100644 --- a/android/Omnivore/app/build.gradle.kts +++ b/android/Omnivore/app/build.gradle.kts @@ -27,8 +27,8 @@ android { applicationId = "app.omnivore.omnivore" minSdk = 26 targetSdk = 34 - versionCode = 200001 - versionName = "0.200.1" + versionCode = 200002 + versionName = "0.200.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt index 4b77f3267..f843734f7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt @@ -21,5 +21,7 @@ object DatabaseModule { context, OmnivoreDatabase::class.java, "omnivore-database", - ).build() + ) + .fallbackToDestructiveMigration() + .build() } From efa2b12e36ae2dab8467a32f216fd9a4df2f2ce6 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 8 Apr 2024 18:34:41 +0800 Subject: [PATCH 16/16] Bump android to 200004 --- android/Omnivore/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/build.gradle.kts b/android/Omnivore/app/build.gradle.kts index fbd681889..ea62ccd25 100644 --- a/android/Omnivore/app/build.gradle.kts +++ b/android/Omnivore/app/build.gradle.kts @@ -27,8 +27,8 @@ android { applicationId = "app.omnivore.omnivore" minSdk = 26 targetSdk = 34 - versionCode = 200002 - versionName = "0.200.2" + versionCode = 200004 + versionName = "0.200.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables {