mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #2899 from omnivore-app/main
Web production deployment
This commit is contained in:
commit
2caa0f1c1c
71 changed files with 987 additions and 575 deletions
26
.github/workflows/run-tests.yaml
vendored
26
.github/workflows/run-tests.yaml
vendored
|
|
@ -14,7 +14,7 @@ on:
|
|||
jobs:
|
||||
run-code-tests:
|
||||
name: Run Codebase tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-m
|
||||
services:
|
||||
postgres:
|
||||
image: ankane/pgvector
|
||||
|
|
@ -27,21 +27,6 @@ jobs:
|
|||
--health-retries 5
|
||||
ports:
|
||||
- 5432
|
||||
elastic:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
|
||||
env:
|
||||
discovery.type: single-node
|
||||
http.cors.allow-origin: '*'
|
||||
http.cors.enabled: true
|
||||
http.cors.allow-headers: 'X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization'
|
||||
http.cors.allow-credentials: true
|
||||
options: >-
|
||||
--health-cmd "curl http://0.0.0.0:9200/_cluster/health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
ports:
|
||||
- 9200
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
|
|
@ -82,13 +67,15 @@ jobs:
|
|||
PG_USER: postgres
|
||||
PG_PASSWORD: postgres
|
||||
PG_DB: omnivore_test
|
||||
ELASTIC_URL: http://localhost:${{ job.services.elastic.ports[9200] }}/
|
||||
PGPASSWORD: postgres # This is required for the psql command to work without a password prompt
|
||||
- name: TypeScript, Lint, Tests
|
||||
- name: TypeScript Build and Lint
|
||||
run: |
|
||||
source ~/.nvm/nvm.sh
|
||||
yarn build
|
||||
yarn lint
|
||||
- name: Tests
|
||||
run: |
|
||||
source ~/.nvm/nvm.sh
|
||||
yarn test
|
||||
env:
|
||||
PG_HOST: localhost
|
||||
|
|
@ -96,8 +83,7 @@ jobs:
|
|||
PG_USER: app_user
|
||||
PG_PASSWORD: app_pass
|
||||
PG_DB: omnivore_test
|
||||
PG_POOL_MAX: 10
|
||||
ELASTIC_URL: http://localhost:${{ job.services.elastic.ports[9200] }}/
|
||||
PG_LOGGER: debug
|
||||
REDIS_URL: redis://localhost:${{ job.services.redis.ports[6379] }}
|
||||
build-docker-images:
|
||||
name: Build docker images
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Omnivore"
|
||||
android:largeHeap="true"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
suspend fun DataService.createWebHighlight(jsonString: String) {
|
||||
suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) {
|
||||
val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
@ -28,7 +28,7 @@ suspend fun DataService.createWebHighlight(jsonString: String) {
|
|||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = false,
|
||||
color = null,
|
||||
color = colorName ?: createHighlightInput.color.getOrNull(),
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
|
@ -80,13 +80,13 @@ suspend fun DataService.createNoteHighlight(savedItemId: String, note: String):
|
|||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val newHighlight = networker.createHighlight(input = CreateHighlightParams(
|
||||
type = HighlightType.NOTE,
|
||||
articleId = savedItemId,
|
||||
id = createHighlightId,
|
||||
shortId = shortId,
|
||||
quote = null,
|
||||
patch = null,
|
||||
annotation = note,
|
||||
type = HighlightType.NOTE,
|
||||
articleId = savedItemId,
|
||||
id = createHighlightId,
|
||||
shortId = shortId,
|
||||
quote = null,
|
||||
patch = null,
|
||||
annotation = note,
|
||||
).asCreateHighlightInput())
|
||||
|
||||
newHighlight?.let {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package app.omnivore.omnivore.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ResourceProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
fun getString(@StringRes stringResId: Int): String {
|
||||
return context.getString(stringResId)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
|
@ -28,8 +29,8 @@ fun AppleAuthButton(viewModel: LoginViewModel) {
|
|||
val showDialog = remember { mutableStateOf(false) }
|
||||
|
||||
LoadingButtonWithIcon(
|
||||
text = "Continue with Apple",
|
||||
loadingText = "Signing in...",
|
||||
text = stringResource(R.string.apple_auth_text),
|
||||
loadingText = stringResource(R.string.apple_auth_loading),
|
||||
isLoading = viewModel.isLoading,
|
||||
icon = painterResource(id = R.drawable.ic_logo_apple),
|
||||
modifier = Modifier.padding(vertical = 6.dp),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
|
|
@ -25,7 +24,6 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import org.intellij.lang.annotations.JdkConstants
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
|
|
@ -42,7 +40,7 @@ fun CreateUserProfileView(viewModel: LoginViewModel) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Create Your Profile",
|
||||
text = stringResource(R.string.create_user_profile_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
|
@ -61,11 +59,11 @@ fun CreateUserProfileView(viewModel: LoginViewModel) {
|
|||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text("Loading...")
|
||||
Text(stringResource(R.string.create_user_profile_loading))
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Cancel Sign Up"),
|
||||
text = AnnotatedString(stringResource(R.string.create_user_profile_action_cancel)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.cancelNewUserSignUp() }
|
||||
|
|
@ -98,8 +96,8 @@ fun UserProfileFields(
|
|||
) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
placeholder = { Text(text = "Name") },
|
||||
label = { Text(text = "Name") },
|
||||
placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_name)) },
|
||||
label = { Text(stringResource(R.string.create_user_profile_field_label_name)) },
|
||||
onValueChange = onNameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
|
|
@ -111,8 +109,8 @@ fun UserProfileFields(
|
|||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
placeholder = { Text(text = "Username") },
|
||||
label = { Text(text = "Username") },
|
||||
placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_username)) },
|
||||
label = { Text(stringResource(R.string.create_user_profile_field_label_username)) },
|
||||
onValueChange = onUsernameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
|
|
@ -144,7 +142,7 @@ fun UserProfileFields(
|
|||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Please enter a valid name and username.",
|
||||
context.getString(R.string.create_user_profile_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
|
@ -154,7 +152,7 @@ fun UserProfileFields(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Submit",
|
||||
text = stringResource(R.string.create_user_profile_action_submit),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
package app.omnivore.omnivore.ui.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
|
|
@ -16,10 +12,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -27,8 +23,8 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
|
|
@ -55,28 +51,28 @@ fun EmailLoginView(viewModel: LoginViewModel) {
|
|||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text("Loading...")
|
||||
Text(stringResource(R.string.email_login_loading))
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString("Return to Social Login"),
|
||||
text = AnnotatedString(stringResource(R.string.email_login_action_back)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSocialLogin() }
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Don't have an account?"),
|
||||
text = AnnotatedString(stringResource(R.string.email_login_action_no_account)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignUp() }
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Forgot your password?"),
|
||||
text = AnnotatedString(stringResource(R.string.email_login_action_forgot_password)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
|
|
@ -111,8 +107,8 @@ fun LoginFields(
|
|||
) {
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
placeholder = { Text(text = "user@email.com") },
|
||||
label = { Text(text = "Email") },
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_email)) },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
|
|
@ -123,8 +119,8 @@ fun LoginFields(
|
|||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
placeholder = { Text(text = "Password") },
|
||||
label = { Text(text = "Password") },
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_password)) },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
|
@ -141,7 +137,7 @@ fun LoginFields(
|
|||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Please enter an email address and password.",
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
|
@ -151,7 +147,7 @@ fun LoginFields(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Login",
|
||||
text = stringResource(R.string.email_login_action_login),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -25,6 +26,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@Composable
|
||||
fun EmailSignUpView(viewModel: LoginViewModel) {
|
||||
|
|
@ -32,8 +34,6 @@ fun EmailSignUpView(viewModel: LoginViewModel) {
|
|||
val email = viewModel.pendingEmailUserCreds?.email ?: ""
|
||||
val password = viewModel.pendingEmailUserCreds?.password ?: ""
|
||||
|
||||
val verificationMessage = "We've sent a verification email to ${email}. Please verify your email and then tap the button below."
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
|
@ -43,7 +43,7 @@ fun EmailSignUpView(viewModel: LoginViewModel) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = verificationMessage,
|
||||
text = stringResource(R.string.email_signup_verification_message, email),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
|
|
@ -55,13 +55,13 @@ fun EmailSignUpView(viewModel: LoginViewModel) {
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Check Status",
|
||||
text = stringResource(R.string.email_signup_check_status),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Use a different email?"),
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_use_different_email)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignUp() }
|
||||
|
|
@ -115,21 +115,21 @@ fun EmailSignUpForm(viewModel: LoginViewModel) {
|
|||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text("Loading...")
|
||||
Text(stringResource(R.string.email_signup_loading))
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString("Return to Social Login"),
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_back)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSocialLogin() }
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Already have an account?"),
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_already_have_account)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignIn() }
|
||||
|
|
@ -167,8 +167,8 @@ fun EmailSignUpFields(
|
|||
) {
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
placeholder = { Text(text = "user@email.com") },
|
||||
label = { Text(text = "Email") },
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
|
|
@ -176,8 +176,8 @@ fun EmailSignUpFields(
|
|||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
placeholder = { Text(text = "Password") },
|
||||
label = { Text(text = "Password") },
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
|
|
@ -186,8 +186,8 @@ fun EmailSignUpFields(
|
|||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
placeholder = { Text(text = "Name") },
|
||||
label = { Text(text = "Name") },
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_name)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_name)) },
|
||||
onValueChange = onNameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
|
|
@ -199,8 +199,8 @@ fun EmailSignUpFields(
|
|||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
placeholder = { Text(text = "Username") },
|
||||
label = { Text(text = "Username") },
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_username)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_username)) },
|
||||
onValueChange = onUsernameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
|
|
@ -231,7 +231,7 @@ fun EmailSignUpFields(
|
|||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Please complete all fields.",
|
||||
context.getString(R.string.email_signup_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
|
@ -241,7 +241,7 @@ fun EmailSignUpFields(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Sign Up",
|
||||
text = stringResource(R.string.email_signup_action_sign_up),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
|
|
@ -38,8 +39,8 @@ fun GoogleAuthButton(viewModel: LoginViewModel) {
|
|||
}
|
||||
|
||||
LoadingButtonWithIcon(
|
||||
text = "Continue with Google",
|
||||
loadingText = "Signing in...",
|
||||
text = stringResource(R.string.google_auth_text),
|
||||
loadingText = stringResource(R.string.google_auth_loading),
|
||||
isLoading = viewModel.isLoading,
|
||||
icon = painterResource(id = R.drawable.ic_logo_google),
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import app.omnivore.omnivore.dataService.DataService
|
|||
import app.omnivore.omnivore.graphql.generated.ValidateUsernameQuery
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.networking.viewer
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
|
|
@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
enum class RegistrationState {
|
||||
SocialLogin,
|
||||
EmailSignIn,
|
||||
|
|
@ -40,7 +42,8 @@ class LoginViewModel @Inject constructor(
|
|||
private val datastoreRepo: DatastoreRepository,
|
||||
private val eventTracker: EventTracker,
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService
|
||||
private val dataService: DataService,
|
||||
private val resourceProvider: ResourceProvider
|
||||
): ViewModel() {
|
||||
private var validateUsernameJob: Job? = null
|
||||
|
||||
|
|
@ -76,7 +79,7 @@ class LoginViewModel @Inject constructor(
|
|||
datastoreRepo.putString(DatastoreKeys.omnivoreSelfHostedWebServer, webServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Self-hosting settings updated.",
|
||||
context.getString(R.string.login_view_model_self_hosting_settings_updated),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
|
@ -88,7 +91,7 @@ class LoginViewModel @Inject constructor(
|
|||
datastoreRepo.clearValue(DatastoreKeys.omnivoreSelfHostedWebServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Self-hosting settings reset.",
|
||||
context.getString(R.string.login_view_model_self_hosting_settings_reset),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
|
@ -156,7 +159,8 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
if (potentialUsername.length < 4 || potentialUsername.length > 15) {
|
||||
usernameValidationErrorMessage = "Username must be between 4 and 15 characters long."
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_validation_length_error_msg)
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -166,7 +170,8 @@ class LoginViewModel @Inject constructor(
|
|||
.matches()
|
||||
|
||||
if (!isValidPattern) {
|
||||
usernameValidationErrorMessage = "Username can contain only letters and numbers"
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_validation_alphanumeric_error_msg)
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -185,11 +190,13 @@ class LoginViewModel @Inject constructor(
|
|||
hasValidUsername = true
|
||||
} else {
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = "This username is not available."
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_not_available_error_msg)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = "Sorry we're having trouble connecting to the server."
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_connection_error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -216,7 +223,8 @@ class LoginViewModel @Inject constructor(
|
|||
if (result.body()?.authToken != null) {
|
||||
datastoreRepo.putString(DatastoreKeys.omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = "Something went wrong. Please check your email/password and try again"
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg)
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
|
|
@ -251,7 +259,8 @@ class LoginViewModel @Inject constructor(
|
|||
isLoading = false
|
||||
|
||||
if (result.errorBody() != null) {
|
||||
errorMessage = "Something went wrong. Please check your entries and try again"
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
} else {
|
||||
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
|
||||
}
|
||||
|
|
@ -284,7 +293,8 @@ class LoginViewModel @Inject constructor(
|
|||
if (result.body()?.authToken != null) {
|
||||
datastoreRepo.putString(DatastoreKeys.omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = "Something went wrong. Please check your email/password and try again"
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg)
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
|
|
@ -315,7 +325,7 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun showGoogleErrorMessage() {
|
||||
errorMessage = "Failed to authenticate with Google."
|
||||
errorMessage = resourceProvider.getString(R.string.login_view_model_google_auth_error_msg)
|
||||
}
|
||||
|
||||
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
|
||||
|
|
@ -324,7 +334,8 @@ class LoginViewModel @Inject constructor(
|
|||
|
||||
// If token is missing then set the error message
|
||||
if (googleIdToken == null) {
|
||||
errorMessage = "No authentication token found."
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_missing_auth_token_error_msg)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -361,10 +372,12 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
418 -> {
|
||||
// Show pending email state
|
||||
errorMessage = "Something went wrong. Please check your credentials and try again"
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
}
|
||||
else -> {
|
||||
errorMessage = "Something went wrong. Please check your credentials and try again"
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -386,7 +399,8 @@ class LoginViewModel @Inject constructor(
|
|||
)
|
||||
registrationStateLiveData.value = RegistrationState.PendingUser
|
||||
} else {
|
||||
errorMessage = "Something went wrong. Please check your credentials and try again"
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.RectangleShape
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -34,6 +35,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||
import androidx.core.content.ContextCompat
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
|
|
@ -62,7 +64,7 @@ fun SelfHostedView(viewModel: LoginViewModel) {
|
|||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text("Loading...")
|
||||
Text(stringResource(R.string.self_hosted_view_loading))
|
||||
}
|
||||
|
||||
Row(
|
||||
|
|
@ -72,14 +74,14 @@ fun SelfHostedView(viewModel: LoginViewModel) {
|
|||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString("Reset"),
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_reset)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.resetSelfHostingDetails(context) },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
ClickableText(
|
||||
text = AnnotatedString("Back"),
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_back)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSocialLogin() },
|
||||
|
|
@ -91,7 +93,7 @@ fun SelfHostedView(viewModel: LoginViewModel) {
|
|||
// "your private self-hosted instance.\n\n"
|
||||
// )
|
||||
ClickableText(
|
||||
text = AnnotatedString("Learn more about self-hosting Omnivore"),
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_learn_more)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
|
|
@ -130,7 +132,7 @@ fun SelfHostedFields(
|
|||
OutlinedTextField(
|
||||
value = apiServer,
|
||||
placeholder = { Text(text = "https://api-prod.omnivore.app/") },
|
||||
label = { Text(text = "API Server") },
|
||||
label = { Text(stringResource(R.string.self_hosted_view_field_api_url_label)) },
|
||||
onValueChange = onAPIServerChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
|
|
@ -142,7 +144,7 @@ fun SelfHostedFields(
|
|||
OutlinedTextField(
|
||||
value = webServer,
|
||||
placeholder = { Text(text = "https://omnivore.app/") },
|
||||
label = { Text(text = "Web Server") },
|
||||
label = { Text(stringResource(R.string.self_hosted_view_field_web_url_label)) },
|
||||
onValueChange = onWebServerChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
|
|
@ -158,7 +160,7 @@ fun SelfHostedFields(
|
|||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Please enter API Server and Web server addresses.",
|
||||
context.getString(R.string.self_hosted_view_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
|
@ -168,7 +170,7 @@ fun SelfHostedFields(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Save",
|
||||
text = stringResource(R.string.self_hosted_view_action_save),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ fun AuthProviderView(viewModel: LoginViewModel) {
|
|||
AppleAuthButton(viewModel)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Continue with Email"),
|
||||
text = AnnotatedString(stringResource(R.string.welcome_screen_action_continue_with_email)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignIn() }
|
||||
|
|
@ -144,7 +144,7 @@ fun AuthProviderView(viewModel: LoginViewModel) {
|
|||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("Self-hosting options"),
|
||||
text = AnnotatedString(stringResource(R.string.welcome_screen_action_self_hosting_options)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSelfHostedSettings() },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
data class HighlightColor(
|
||||
val name: String = "yellow",
|
||||
val color: Color = Color(0xFFFFD234),
|
||||
)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.ui.components.HighlightColor
|
||||
import app.omnivore.omnivore.ui.components.HighlightColorPaletteMode
|
||||
|
||||
@Composable
|
||||
fun HighlightColorPalette(
|
||||
mode: HighlightColorPaletteMode = HighlightColorPaletteMode.Light,
|
||||
selectedColorName: String,
|
||||
onColorSelected: (color: HighlightColor) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = mode.backgroundColor,
|
||||
shadowElevation = 9.dp
|
||||
) {
|
||||
Row(modifier = Modifier.padding(8.dp, 2.dp, 8.dp, 2.dp)) {
|
||||
HighlightColorPaletteItem(
|
||||
color = HighlightColor(name = "yellow", Color(0xFFFFD234)),
|
||||
isSelected = "yellow" == selectedColorName,
|
||||
onClick = onColorSelected
|
||||
)
|
||||
HighlightColorPaletteItem(
|
||||
color = HighlightColor(name = "red", Color(0xFFFB9A9A)),
|
||||
isSelected = "red" == selectedColorName,
|
||||
onClick = onColorSelected
|
||||
)
|
||||
HighlightColorPaletteItem(
|
||||
color = HighlightColor(name = "green", Color(0xFF55C689)),
|
||||
isSelected = "green" == selectedColorName,
|
||||
onClick = onColorSelected
|
||||
)
|
||||
HighlightColorPaletteItem(
|
||||
color = HighlightColor(name = "blue", Color(0xFF6AB1FF)),
|
||||
isSelected = "blue" == selectedColorName,
|
||||
onClick = onColorSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.ui.components.HighlightColor
|
||||
|
||||
@Composable
|
||||
fun HighlightColorPaletteItem(
|
||||
color: HighlightColor,
|
||||
isSelected: Boolean,
|
||||
onClick: (color: HighlightColor) -> Unit,
|
||||
modifier: Modifier = Modifier.padding(6.dp)
|
||||
) {
|
||||
Column (
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color.color)
|
||||
.clickable { onClick(color) }
|
||||
)
|
||||
{
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
Icons.Rounded.Check,
|
||||
contentDescription = "checkIcon",
|
||||
tint = Color.DarkGray,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package app.omnivore.omnivore.ui.components
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
enum class HighlightColorPaletteMode(val backgroundColor: Color) {
|
||||
Light(Color.White),
|
||||
Dark(Color.Black),
|
||||
}
|
||||
|
|
@ -20,10 +20,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -59,13 +61,13 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: (String, String) -> Unit)
|
|||
.fillMaxWidth()
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = "Cancel")
|
||||
Text(text = stringResource(R.string.label_creation_action_cancel))
|
||||
}
|
||||
|
||||
Text("Create New Label", fontWeight = FontWeight.ExtraBold)
|
||||
Text(stringResource(R.string.label_creation_title), fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
TextButton(onClick = { onSave(labelName, selectedHex) }) {
|
||||
Text(text = "Create")
|
||||
Text(text = stringResource(R.string.label_creation_action_create))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: (String, String) -> Unit)
|
|||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp)
|
||||
) {
|
||||
Text("Assign a name and color.")
|
||||
Text(stringResource(R.string.label_creation_content))
|
||||
}
|
||||
|
||||
Row(
|
||||
|
|
@ -88,7 +90,7 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: (String, String) -> Unit)
|
|||
) {
|
||||
OutlinedTextField(
|
||||
value = labelName,
|
||||
placeholder = { Text(text = "Label Name") },
|
||||
placeholder = { Text(stringResource(R.string.label_creation_label_placeholder)) },
|
||||
onValueChange = { labelName = it },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
|
|
@ -44,6 +45,7 @@ import androidx.compose.ui.text.toLowerCase
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
|
|
@ -260,7 +262,9 @@ fun LabelsSelectionSheetContent(
|
|||
it.name.toLowerCase(Locale.current) == text
|
||||
}
|
||||
|
||||
val titleText = if (isLibraryMode) "Filter by Label" else "Set Labels"
|
||||
val titleText = if (isLibraryMode)
|
||||
stringResource(R.string.label_selection_sheet_title) else
|
||||
stringResource(R.string.label_selection_sheet_title_alt)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
|
|
@ -282,13 +286,15 @@ fun LabelsSelectionSheetContent(
|
|||
.fillMaxWidth()
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = "Cancel")
|
||||
Text(text = stringResource(R.string.label_selection_sheet_action_cancel))
|
||||
}
|
||||
|
||||
Text(titleText, fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
TextButton(onClick = { onSave(state.chips.map { it.label }) }) {
|
||||
Text(text = if (isLibraryMode) "Search" else "Save")
|
||||
Text(text = if (isLibraryMode)
|
||||
stringResource(R.string.label_selection_sheet_action_search) else
|
||||
stringResource(R.string.label_selection_sheet_action_save))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +347,11 @@ fun LabelsSelectionSheetContent(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val label = findOrCreateLabel(labelsViewModel = labelsViewModel, labels = labels, name = filterTextValue)
|
||||
val label = findOrCreateLabel(
|
||||
labelsViewModel = labelsViewModel,
|
||||
labels = labels,
|
||||
name = filterTextValue
|
||||
)
|
||||
state.addChip(LabelChipView(label))
|
||||
filterTextValue = TextFieldValue()
|
||||
}
|
||||
|
|
@ -354,7 +364,7 @@ fun LabelsSelectionSheetContent(
|
|||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(text = "Create a new label named \"${filterTextValue.text}\"")
|
||||
Text(text = stringResource(R.string.label_selection_sheet_text_create, filterTextValue.text))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -373,6 +383,7 @@ fun LabelsSelectionSheetContent(
|
|||
name = label.name,
|
||||
colors = chipColors,
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp, bottom = 10.dp)
|
||||
.clickable {
|
||||
state.addChip(LabelChipView(label))
|
||||
filterTextValue = TextFieldValue()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -69,7 +70,7 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) {
|
|||
)
|
||||
AssistChip(
|
||||
onClick = { viewModel.showLabelsSelectionSheetLiveData.value = true },
|
||||
label = { Text("Labels") },
|
||||
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
|
|
@ -34,7 +35,9 @@ fun LibraryNavigationBar(
|
|||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(if (actionsMenuItem == null) "Library" else "")
|
||||
Text(if (actionsMenuItem == null)
|
||||
stringResource(R.string.library_nav_bar_title) else
|
||||
stringResource(R.string.library_nav_bar_title_alt))
|
||||
},
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
|
|
@ -218,7 +221,7 @@ fun SearchField(
|
|||
value = searchText,
|
||||
onValueChange = onSearchTextChanged,
|
||||
placeholder = {
|
||||
Text(text = "Search")
|
||||
Text(text = stringResource(R.string.library_nav_bar_field_placeholder_search))
|
||||
},
|
||||
leadingIcon = {
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
|||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -31,7 +32,8 @@ import javax.inject.Inject
|
|||
class LibraryViewModel @Inject constructor(
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val resourceProvider: ResourceProvider
|
||||
): ViewModel(), SavedItemViewModel {
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
|
||||
|
|
@ -348,9 +350,9 @@ class LibraryViewModel @Inject constructor(
|
|||
dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
if (!networkResult || labelCreationError) {
|
||||
snackbarMessage = "Unable to set labels"
|
||||
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_error)
|
||||
} else {
|
||||
snackbarMessage = "Labels updated"
|
||||
snackbarMessage = resourceProvider.getString(R.string.library_view_model_snackbar_success)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import kotlinx.coroutines.launch
|
|||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
|
||||
fun notebookMD(notes: List<Highlight>, highlights: List<Highlight>): String {
|
||||
|
|
@ -80,6 +81,9 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditNote:
|
|||
val scrollState = rememberScrollState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val context = LocalContext.current
|
||||
val snackBarHostStateMsg = context.getString(R.string.notebook_view_snackbar_msg)
|
||||
|
||||
val clipboard: ClipboardManager? =
|
||||
LocalContext.current.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
|
||||
|
|
@ -89,7 +93,7 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditNote:
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Notebook") },
|
||||
title = { Text(stringResource(R.string.notebook_view_title)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
|
|
@ -109,7 +113,7 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditNote:
|
|||
onDismissRequest = { isMenuOpen = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
text = { Text(stringResource(R.string.notebook_view_action_copy)) },
|
||||
onClick = {
|
||||
val clip = ClipData.newPlainText("notebook", notebookMD(notes, highlights))
|
||||
clipboard?.let {
|
||||
|
|
@ -117,7 +121,7 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditNote:
|
|||
} ?: run {
|
||||
coroutineScope.launch {
|
||||
snackBarHostState
|
||||
.showSnackbar("Notebook copied")
|
||||
.showSnackbar(snackBarHostStateMsg)
|
||||
}
|
||||
}
|
||||
isMenuOpen = false
|
||||
|
|
@ -154,7 +158,7 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String
|
|||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text("Note") },
|
||||
title = { Text(stringResource(R.string.edit_note_modal_title)) },
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
|
|
@ -163,14 +167,14 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String
|
|||
TextButton(onClick = {
|
||||
onDismiss(false, initialValue)
|
||||
}) {
|
||||
Text(text = "Cancel")
|
||||
Text(text = stringResource(R.string.edit_note_modal_action_cancel))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(onClick = {
|
||||
onDismiss(true, annotation.value)
|
||||
}) {
|
||||
Text(text = "Save")
|
||||
Text(text = stringResource(R.string.edit_note_modal_action_save))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -205,7 +209,7 @@ fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighl
|
|||
.padding(start = 15.dp)
|
||||
.padding(top = 20.dp, bottom = 50.dp)
|
||||
) {
|
||||
Text("Article Notes")
|
||||
Text(stringResource(R.string.article_notes_title))
|
||||
Divider(modifier = Modifier.padding(bottom= 15.dp))
|
||||
notes.forEach { note ->
|
||||
MarkdownText(
|
||||
|
|
@ -231,7 +235,7 @@ fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighl
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Add Notes...",
|
||||
text = stringResource(R.string.article_notes_action_add_notes),
|
||||
style = androidx.compose.material.MaterialTheme.typography.subtitle2,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 2.dp, horizontal = 0.dp),
|
||||
|
|
@ -250,6 +254,8 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Hi
|
|||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val context = LocalContext.current
|
||||
val snackBarHostStateMsg = context.getString(R.string.highlights_list_snackbar_msg)
|
||||
val clipboard: ClipboardManager? =
|
||||
LocalContext.current.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
|
||||
|
|
@ -258,7 +264,7 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Hi
|
|||
.padding(start = 15.dp)
|
||||
.padding(top = 40.dp, bottom = 100.dp)
|
||||
) {
|
||||
Text("Highlights")
|
||||
Text(stringResource(R.string.highlights_list_title))
|
||||
Divider(modifier = Modifier.padding(bottom= 10.dp))
|
||||
highlights.forEach { highlight ->
|
||||
var isMenuOpen by remember { mutableStateOf(false) }
|
||||
|
|
@ -282,7 +288,7 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Hi
|
|||
onDismissRequest = { isMenuOpen = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
text = { Text(stringResource(R.string.highlights_list_action_copy)) },
|
||||
onClick = {
|
||||
val clip = ClipData.newPlainText("highlight", highlight.quote)
|
||||
clipboard?.let {
|
||||
|
|
@ -290,7 +296,7 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Hi
|
|||
} ?: run {
|
||||
coroutineScope.launch {
|
||||
snackBarHostState
|
||||
.showSnackbar("Highlight copied")
|
||||
.showSnackbar(snackBarHostStateMsg)
|
||||
}
|
||||
}
|
||||
isMenuOpen = false
|
||||
|
|
@ -354,7 +360,7 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Hi
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Add Note...",
|
||||
text = stringResource(R.string.highlights_list_action_add_note),
|
||||
style = androidx.compose.material.MaterialTheme.typography.subtitle2,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 2.dp, horizontal = 0.dp),
|
||||
|
|
@ -365,7 +371,7 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Hi
|
|||
}
|
||||
if (highlights.isEmpty()) {
|
||||
Text(
|
||||
text = "You have not added any highlights to this page.",
|
||||
text = stringResource(R.string.highlights_list_error_msg_no_highlights),
|
||||
style = androidx.compose.material.MaterialTheme.typography.subtitle2,
|
||||
modifier = Modifier.padding(vertical = 10.dp, horizontal = 10.dp)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -103,7 +104,7 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
Text("Font Size:", style = TextStyle(
|
||||
Text(stringResource(R.string.reader_preferences_view_font_size), style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
|
|
@ -118,7 +119,7 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
valueRange = 10f..48f,
|
||||
)
|
||||
|
||||
Text("Margin", style = TextStyle(
|
||||
Text(stringResource(R.string.reader_preferences_view_margin), style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
|
|
@ -133,7 +134,7 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
valueRange = 60f..100f,
|
||||
)
|
||||
|
||||
Text("Line Spacing", style = TextStyle(
|
||||
Text(stringResource(R.string.reader_preferences_view_line_spacing), style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
|
|
@ -153,13 +154,13 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text("Theme:", style = TextStyle(
|
||||
Text(stringResource(R.string.reader_preferences_view_theme), style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Text("Auto", style = TextStyle(
|
||||
Text(stringResource(R.string.reader_preferences_view_auto), style = TextStyle(
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
|
|
@ -208,7 +209,8 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("High Contrast Text",
|
||||
Text(
|
||||
stringResource(R.string.reader_preferences_view_high_constrast_text),
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
|
|
@ -225,7 +227,8 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
|||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Justify Text",
|
||||
Text(
|
||||
stringResource(R.string.reader_preferences_view_justify_text),
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import HighlightColorPalette
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
|
|
@ -18,8 +19,12 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.components.HighlightColorPaletteMode
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -40,6 +45,9 @@ fun WebReader(
|
|||
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
val showHighlightColorPalette = webReaderViewModel.showHighlightColorPalette.observeAsState()
|
||||
val highlightColor = webReaderViewModel.highlightColor.observeAsState()
|
||||
|
||||
Box {
|
||||
AndroidView(factory = {
|
||||
OmnivoreWebView(it).apply {
|
||||
|
|
@ -144,6 +152,18 @@ fun WebReader(
|
|||
webReaderViewModel.resetJavascriptDispatchQueue()
|
||||
}
|
||||
})
|
||||
if (showHighlightColorPalette.value == true) {
|
||||
HighlightColorPalette(
|
||||
mode = if (isDarkMode) HighlightColorPaletteMode.Dark else HighlightColorPaletteMode.Light,
|
||||
selectedColorName = highlightColor.value?.name ?: "yellow",
|
||||
onColorSelected = {
|
||||
webReaderViewModel.setHighlightColor(it)
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(12.dp, 12.dp, 12.dp, 36.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +184,7 @@ class OmnivoreWebView(context: Context) : WebView(context), OnScrollChangeListen
|
|||
Log.d("wv", "inflating existing highlight menu")
|
||||
mode.menuInflater.inflate(R.menu.highlight_selection_menu, menu)
|
||||
} else {
|
||||
viewModel?.showHighlightColorPalette()
|
||||
mode.menuInflater.inflate(R.menu.text_selection_menu, menu)
|
||||
}
|
||||
return true
|
||||
|
|
@ -233,6 +254,7 @@ class OmnivoreWebView(context: Context) : WebView(context), OnScrollChangeListen
|
|||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
Log.d("wv", "destroying menu: $mode")
|
||||
viewModel?.hasTappedExistingHighlight = false
|
||||
viewModel?.hideHighlightColorPalette()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlin.math.roundToInt
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.omnivore.omnivore.ui.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.ui.notebook.EditNoteModal
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
|
|||
.background(color = Color.Black)
|
||||
) {
|
||||
if (viewModel.hasFetchError.value == true) {
|
||||
Text("We were unable to fetch your content.")
|
||||
Text(stringResource(R.string.web_reader_loading_container_error_msg))
|
||||
} else {
|
||||
WebReaderLoadingContainer(
|
||||
requestID = requestID,
|
||||
|
|
@ -200,13 +201,13 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
|
|||
sheetContent = {
|
||||
when (bottomSheetState) {
|
||||
BottomSheetState.PREFERENCES -> {
|
||||
BottomSheetUI("Reader Preferences") {
|
||||
BottomSheetUI(stringResource(R.string.web_reader_loading_container_bottom_sheet_reader_preferences)) {
|
||||
ReaderPreferencesView(webReaderViewModel)
|
||||
}
|
||||
}
|
||||
BottomSheetState.NOTEBOOK -> {
|
||||
webReaderParams?.let { params ->
|
||||
BottomSheetUI(title = "Notebook") {
|
||||
BottomSheetUI(title = stringResource(R.string.web_reader_loading_container_bottom_sheet_notebook)) {
|
||||
NotebookView(savedItemId = params.item.savedItemId, viewModel = notebookViewModel, onEditNote = {
|
||||
notebookViewModel.highlightUnderEdit = it
|
||||
webReaderViewModel.setBottomSheet(BottomSheetState.EDITNOTE)
|
||||
|
|
@ -255,7 +256,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
|
|||
)
|
||||
}
|
||||
BottomSheetState.LABELS -> {
|
||||
BottomSheetUI(title = "Notebook") {
|
||||
BottomSheetUI(title = stringResource(R.string.web_reader_loading_container_bottom_sheet_notebook)) {
|
||||
LabelsSelectionSheetContent(
|
||||
labels = labels,
|
||||
labelsViewModel = labelsViewModel,
|
||||
|
|
@ -283,7 +284,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
|
|||
}
|
||||
}
|
||||
BottomSheetState.LINK -> {
|
||||
BottomSheetUI(title = "Open Link") {
|
||||
BottomSheetUI(title = stringResource(R.string.web_reader_loading_container_bottom_sheet_open_link)) {
|
||||
OpenLinkView(webReaderViewModel)
|
||||
}
|
||||
}
|
||||
|
|
@ -460,25 +461,25 @@ fun OpenLinkView(webReaderViewModel: WebReaderViewModel) {
|
|||
.padding(horizontal = 50.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Row {
|
||||
Button(onClick = { webReaderViewModel.openCurrentLink(context) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Open in Browser")
|
||||
Text(text = stringResource(R.string.open_link_view_action_open_in_browser))
|
||||
|
||||
}
|
||||
}
|
||||
Row() {
|
||||
Button(onClick = { webReaderViewModel.saveCurrentLink(context) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Save to Omnivore")
|
||||
Text(text = stringResource(R.string.open_link_view_action_save_to_omnivore))
|
||||
|
||||
}
|
||||
}
|
||||
Row() {
|
||||
Button(onClick = {webReaderViewModel.copyCurrentLink(context) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Copy Link")
|
||||
Text(text = stringResource(R.string.open_link_view_action_copy_link))
|
||||
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Button(onClick = {webReaderViewModel.resetBottomSheet() }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Cancel")
|
||||
Text(text = stringResource(R.string.open_link_view_action_cancel))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.lifecycle.*
|
|||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.EventTracker
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
||||
|
|
@ -20,6 +21,7 @@ import app.omnivore.omnivore.networking.*
|
|||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.components.HighlightColor
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||
|
|
@ -76,7 +78,10 @@ class WebReaderViewModel @Inject constructor(
|
|||
var lastTapCoordinates: TapCoordinates? = null
|
||||
private var isLoading = false
|
||||
private var slug: String? = null
|
||||
|
||||
|
||||
val showHighlightColorPalette = MutableLiveData(false)
|
||||
val highlightColor = MutableLiveData(HighlightColor())
|
||||
|
||||
fun loadItem(slug: String?, requestID: String?) {
|
||||
this.slug = slug
|
||||
if (isLoading || webReaderParamsLiveData.value != null) { return }
|
||||
|
|
@ -139,7 +144,11 @@ class WebReaderViewModel @Inject constructor(
|
|||
currentLink?.let {
|
||||
viewModelScope.launch {
|
||||
val success = networker.saveUrl(it)
|
||||
Toast.makeText(context, if (success) "Link saved" else "Error saving link" , Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context,
|
||||
if (success)
|
||||
context.getString(R.string.web_reader_view_model_save_link_success) else
|
||||
context.getString(R.string.web_reader_view_model_save_link_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.NONE)
|
||||
|
|
@ -153,7 +162,9 @@ class WebReaderViewModel @Inject constructor(
|
|||
clipboard.setPrimaryClip(clip)
|
||||
clipboard?.let {
|
||||
clipboard?.setPrimaryClip(clip)
|
||||
Toast.makeText(context, "Link Copied", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.web_reader_view_model_copy_link_success),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
bottomSheetStateLiveData.postValue(BottomSheetState.NONE)
|
||||
|
|
@ -290,11 +301,30 @@ class WebReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fun showHighlightColorPalette() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
showHighlightColorPalette.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideHighlightColorPalette() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
showHighlightColorPalette.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun setHighlightColor(color: HighlightColor) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
highlightColor.postValue(color)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
||||
when (actionID) {
|
||||
"createHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.createWebHighlight(jsonString)
|
||||
dataService.createWebHighlight(jsonString, highlightColor.value?.name)
|
||||
}
|
||||
}
|
||||
"deleteHighlight" -> {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import androidx.compose.material.ButtonDefaults
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.MainActivity
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -36,7 +38,7 @@ fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomShee
|
|||
.fillMaxSize()
|
||||
.padding(top = 48.dp, bottom = 32.dp)
|
||||
) {
|
||||
Text(text = viewModel.message ?: "Saving")
|
||||
Text(text = viewModel.message ?: stringResource(R.string.save_content_msg))
|
||||
Row {
|
||||
if (enableReadNow) {
|
||||
Button(
|
||||
|
|
@ -55,7 +57,7 @@ fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomShee
|
|||
backgroundColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(text = "Read Now")
|
||||
Text(text = stringResource(R.string.save_content_action_read_now))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
|
@ -72,7 +74,9 @@ fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomShee
|
|||
backgroundColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(text = if (enableReadNow) "Read Later" else "Dismiss")
|
||||
Text(text = if (enableReadNow)
|
||||
stringResource(R.string.save_content_action_read_later) else
|
||||
stringResource(R.string.save_content_action_dismiss))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ import androidx.lifecycle.viewModelScope
|
|||
import app.omnivore.omnivore.Constants
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.graphql.generated.SaveUrlMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput
|
||||
import app.omnivore.omnivore.ui.ResourceProvider
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -32,7 +34,8 @@ enum class SaveState {
|
|||
|
||||
@HiltViewModel
|
||||
class SaveViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val resourceProvider: ResourceProvider
|
||||
) : ViewModel() {
|
||||
val saveState = MutableLiveData(SaveState.NONE)
|
||||
|
||||
|
|
@ -62,13 +65,13 @@ class SaveViewModel @Inject constructor(
|
|||
fun saveURL(url: String) {
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
message = "Saving to Omnivore..."
|
||||
message = resourceProvider.getString(R.string.save_view_model_msg)
|
||||
saveState.postValue(SaveState.SAVING)
|
||||
|
||||
val authToken = getAuthToken()
|
||||
|
||||
if (authToken == null) {
|
||||
message = "You are not logged in. Please login before saving."
|
||||
message = resourceProvider.getString(R.string.save_view_model_error_not_logged_in)
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -103,15 +106,15 @@ class SaveViewModel @Inject constructor(
|
|||
|
||||
val success = (response.data?.saveUrl?.onSaveSuccess?.url != null)
|
||||
message = if (success) {
|
||||
"Page Saved"
|
||||
resourceProvider.getString(R.string.save_view_model_page_saved_success)
|
||||
} else {
|
||||
"There was an error saving your page"
|
||||
resourceProvider.getString(R.string.save_view_model_page_saved_error)
|
||||
}
|
||||
|
||||
saveState.postValue(SaveState.SAVED)
|
||||
Log.d(ContentValues.TAG, "Saved URL?: $success")
|
||||
} catch (e: java.lang.Exception) {
|
||||
message = "There was an error saving your page"
|
||||
message = resourceProvider.getString(R.string.save_view_model_page_saved_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderViewModel
|
||||
|
|
@ -31,7 +32,7 @@ fun SavedItemContextMenu(
|
|||
onDismissRequest = onDismiss
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Edit Labels") },
|
||||
text = { Text(stringResource(R.string.saved_item_context_menu_action_edit_labels)) },
|
||||
onClick = {
|
||||
actionHandler(SavedItemAction.EditLabels)
|
||||
onDismiss()
|
||||
|
|
@ -44,7 +45,9 @@ fun SavedItemContextMenu(
|
|||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isArchived) "Unarchive" else "Archive") },
|
||||
text = { Text(if (isArchived)
|
||||
stringResource(R.string.saved_item_context_menu_action_unarchive) else
|
||||
stringResource(R.string.saved_item_context_menu_action_archive)) },
|
||||
onClick = {
|
||||
val action = if (isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive
|
||||
actionHandler(action)
|
||||
|
|
@ -58,7 +61,7 @@ fun SavedItemContextMenu(
|
|||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Share Original") },
|
||||
text = { Text(stringResource(R.string.saved_item_context_menu_action_share_original)) },
|
||||
onClick = {
|
||||
webReaderViewModel.showShareLinkSheet(context)
|
||||
onDismiss()
|
||||
|
|
@ -71,7 +74,7 @@ fun SavedItemContextMenu(
|
|||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Remove Item") },
|
||||
text = { Text(stringResource(R.string.saved_item_context_menu_action_remove_item)) },
|
||||
onClick = {
|
||||
actionHandler(SavedItemAction.Delete)
|
||||
onDismiss()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import androidx.compose.material3.Button
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.omnivore.omnivore.R
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
|
||||
|
|
@ -14,9 +16,9 @@ fun LogoutDialog(onClose: (Boolean) -> Unit) {
|
|||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onClose(false) },
|
||||
title = { Text(text = "Logout") },
|
||||
title = { Text(text = stringResource(R.string.logout_dialog_title)) },
|
||||
text = {
|
||||
Text("Are you sure you want to logout?")
|
||||
Text(stringResource(R.string.logout_dialog_confirm_msg))
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
|
|
@ -28,12 +30,12 @@ fun LogoutDialog(onClose: (Boolean) -> Unit) {
|
|||
googleSignIn.signOut()
|
||||
onClose(true)
|
||||
}) {
|
||||
Text("Confirm")
|
||||
Text(stringResource(R.string.logout_dialog_action_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { onClose(false) }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(R.string.logout_dialog_action_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@Composable
|
||||
fun ManageAccountDialog(onDismiss: () -> Unit, settingsViewModel: SettingsViewModel) {
|
||||
|
|
@ -43,7 +45,7 @@ fun ManageAccountView(settingsViewModel: SettingsViewModel) {
|
|||
.padding(top = 12.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("Manage Account")
|
||||
Text(stringResource(R.string.manage_account_title))
|
||||
}
|
||||
|
||||
Column(
|
||||
|
|
@ -55,7 +57,7 @@ fun ManageAccountView(settingsViewModel: SettingsViewModel) {
|
|||
modifier = Modifier
|
||||
.clickable(onClick = { settingsViewModel.resetDataCache() })
|
||||
) {
|
||||
Text("Reset Data Cache")
|
||||
Text(stringResource(R.string.manage_account_action_reset_data_cache))
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Icon(imageVector = Icons.Filled.Refresh, contentDescription = null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import androidx.compose.material.icons.filled.Settings
|
|||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SetJavaScriptEnabled")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -22,7 +24,7 @@ fun PolicyWebView(navController: NavHostController, url: String) {
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { androidx.compose.material3.Text("Settings") },
|
||||
title = { androidx.compose.material3.Text(stringResource(R.string.policy_webview_title)) },
|
||||
actions = {
|
||||
IconButton(onClick = { navController.navigate(Routes.Settings.route) }) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
|
|
@ -37,7 +38,7 @@ fun SettingsView(
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
title = { Text(stringResource(R.string.settings_view_title)) },
|
||||
actions = {
|
||||
IconButton(onClick = { navController.navigate(Routes.Library.route) }) {
|
||||
Icon(
|
||||
|
|
@ -100,19 +101,31 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: Setti
|
|||
//
|
||||
// SectionSpacer()
|
||||
|
||||
SettingRow(text = "Documentation") { navController.navigate(Routes.Documentation.route) }
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_documentation)) {
|
||||
navController.navigate(Routes.Documentation.route)
|
||||
}
|
||||
RowDivider()
|
||||
SettingRow(text = "Feedback") { Intercom.client().present(space = IntercomSpace.Messages) }
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_feedback)) {
|
||||
Intercom.client().present(space = IntercomSpace.Messages)
|
||||
}
|
||||
RowDivider()
|
||||
SettingRow(text = "Privacy Policy") { navController.navigate(Routes.PrivacyPolicy.route) }
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_privacy_policy)) {
|
||||
navController.navigate(Routes.PrivacyPolicy.route)
|
||||
}
|
||||
RowDivider()
|
||||
SettingRow(text = "Terms and Conditions") { navController.navigate(Routes.TermsAndConditions.route) }
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_terms_and_conditions)) {
|
||||
navController.navigate(Routes.TermsAndConditions.route)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SettingRow(text = "Manage Account") { showManageAccountDialog.value = true }
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_manage_account)) {
|
||||
showManageAccountDialog.value = true
|
||||
}
|
||||
RowDivider()
|
||||
SettingRow(text = "Logout", includeIcon = false) { showLogoutDialog.value = true }
|
||||
SettingRow(text = stringResource(R.string.settings_view_setting_row_logout), includeIcon = false) {
|
||||
showLogoutDialog.value = true
|
||||
}
|
||||
RowDivider()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M84.86,53.22C84.86,55.2 84.38,57.6 83.89,59.69V59.71L83.89,59.72C83.29,62.16 81.89,64.33 79.91,65.88C77.93,67.42 75.49,68.26 72.98,68.26H72.84C66.49,68.26 62.49,63.05 62.49,57.21V48.85L57.76,55.86L57.5,56.07C56.52,56.89 55.28,57.34 54,57.34C52.72,57.34 51.48,56.89 50.5,56.07L50.24,55.87L45.37,48.76V66H40.72V47.23C40.72,43.68 44.93,41.61 47.73,44.1L47.94,44.29L53.64,52.61C53.75,52.66 53.87,52.69 53.99,52.69C54.12,52.69 54.23,52.66 54.34,52.61L59.93,44.33L60.18,44.12C62.84,41.91 67.14,43.58 67.14,47.3V57.21C67.14,61 69.55,63.61 72.84,63.61H72.98C76.02,63.61 78.64,61.55 79.37,58.62C79.86,56.54 80.21,54.61 80.21,53.24C80.14,38.26 67.47,26.82 52.13,27.86C39.2,28.75 28.75,39.2 27.85,52.14C27.6,55.72 28.09,59.32 29.3,62.71C30.5,66.1 32.38,69.2 34.84,71.83C37.29,74.45 40.26,76.55 43.55,77.98C46.85,79.41 50.41,80.15 54,80.14V84.79C49.77,84.8 45.58,83.93 41.7,82.24C37.82,80.56 34.33,78.09 31.44,75C28.55,71.91 26.33,68.26 24.91,64.27C23.5,60.28 22.92,56.05 23.21,51.82C24.27,36.56 36.56,24.27 51.81,23.21C69.69,22.01 84.78,35.42 84.86,53.22Z"
|
||||
android:fillColor="#3D3D3D"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -12,4 +12,195 @@
|
|||
<string name="highlight_note">Note</string>
|
||||
<string name="copyTextSelection">Copy</string>
|
||||
<string name="pdf_highlight_menu_note">Note</string>
|
||||
|
||||
<!-- Apple Auth -->
|
||||
<string name="apple_auth_text">Continue with Apple</string>
|
||||
<string name="apple_auth_loading">Signing in...</string>
|
||||
|
||||
<!-- Create User Profile -->
|
||||
<string name="create_user_profile_title">Create Your Profile</string>
|
||||
<string name="create_user_profile_loading">Loading...</string>
|
||||
<string name="create_user_profile_action_cancel">Cancel Sign Up</string>
|
||||
<string name="create_user_profile_action_submit">Submit</string>
|
||||
<string name="create_user_profile_field_placeholder_name">Name</string>
|
||||
<string name="create_user_profile_field_label_name">Name</string>
|
||||
<string name="create_user_profile_field_placeholder_username">Username</string>
|
||||
<string name="create_user_profile_field_label_username">Username</string>
|
||||
<string name="create_user_profile_error_msg">Please enter a valid name and username.</string>
|
||||
|
||||
<!-- Email Login -->
|
||||
<string name="email_login_loading">Loading...</string>
|
||||
<string name="email_login_action_back">Return to Social Login</string>
|
||||
<string name="email_login_action_no_account">Don\'t have an account?</string>
|
||||
<string name="email_login_action_forgot_password">Forgot your password?</string>
|
||||
<string name="email_login_action_login">Login</string>
|
||||
<string name="email_login_field_placeholder_email">user@email.com</string>
|
||||
<string name="email_login_field_label_email">Email</string>
|
||||
<string name="email_login_field_placeholder_password">Password</string>
|
||||
<string name="email_login_field_label_password">Password</string>
|
||||
<string name="email_login_error_msg">Please enter an email address and password.</string>
|
||||
|
||||
<!-- Email Sign Up -->
|
||||
<string name="email_signup_verification_message">We\'ve sent a verification email to %1$s. Please verify your email and then tap the button below.</string>
|
||||
<string name="email_signup_check_status">Check Status</string>
|
||||
<string name="email_signup_action_use_different_email">Use a different email?</string>
|
||||
<string name="email_signup_loading">Loading...</string>
|
||||
<string name="email_signup_action_back">Return to Social Login</string>
|
||||
<string name="email_signup_action_already_have_account">Already have an account?</string>
|
||||
<string name="email_signup_action_sign_up">Sign Up</string>
|
||||
<string name="email_signup_field_placeholder_email">user@email.com</string>
|
||||
<string name="email_signup_field_label_email">Email</string>
|
||||
<string name="email_signup_field_placeholder_password">Password</string>
|
||||
<string name="email_signup_field_label_password">Password</string>
|
||||
<string name="email_signup_field_placeholder_name">Name</string>
|
||||
<string name="email_signup_field_label_name">Name</string>
|
||||
<string name="email_signup_field_placeholder_username">Name</string>
|
||||
<string name="email_signup_field_label_username">Name</string>
|
||||
<string name="email_signup_error_msg">Please complete all fields.</string>
|
||||
|
||||
<!-- Google Auth -->
|
||||
<string name="google_auth_text">Continue with Google</string>
|
||||
<string name="google_auth_loading">Signing in...</string>
|
||||
|
||||
<!-- LoginViewModel -->
|
||||
<string name="login_view_model_self_hosting_settings_updated">Self-hosting settings updated.</string>
|
||||
<string name="login_view_model_self_hosting_settings_reset">Self-hosting settings reset.</string>
|
||||
<string name="login_view_model_username_validation_length_error_msg">Username must be between 4 and 15 characters long.</string>
|
||||
<string name="login_view_model_username_validation_alphanumeric_error_msg">Username can contain only letters and numbers.</string>
|
||||
<string name="login_view_model_username_not_available_error_msg">This username is not available.</string>
|
||||
<string name="login_view_model_connection_error_msg">Sorry we\'re having trouble connecting to the server.</string>
|
||||
<string name="login_view_model_something_went_wrong_error_msg">Something went wrong. Please check your email/password and try again.</string>
|
||||
<string name="login_view_model_something_went_wrong_two_error_msg">Something went wrong. Please check your credentials and try again.</string>
|
||||
<string name="login_view_model_google_auth_error_msg">Failed to authenticate with Google.</string>
|
||||
<string name="login_view_model_missing_auth_token_error_msg">No authentication token found.</string>
|
||||
|
||||
<!-- SelfHostedView -->
|
||||
<string name="self_hosted_view_loading">Loading...</string>
|
||||
<string name="self_hosted_view_action_reset">Reset</string>
|
||||
<string name="self_hosted_view_action_back">Back</string>
|
||||
<string name="self_hosted_view_action_save">Save</string>
|
||||
<string name="self_hosted_view_action_learn_more">Learn more about self-hosting Omnivore</string>
|
||||
<string name="self_hosted_view_field_api_url_label">API Server</string>
|
||||
<string name="self_hosted_view_field_web_url_label">Web Server</string>
|
||||
<string name="self_hosted_view_error_msg">Please enter API Server and Web server addresses.</string>
|
||||
|
||||
<!-- WelcomeScreen -->
|
||||
<string name="welcome_screen_action_dismiss">Dismiss</string>
|
||||
<string name="welcome_screen_action_continue_with_email">Continue with Email</string>
|
||||
<string name="welcome_screen_action_self_hosting_options">Self-hosting options</string>
|
||||
|
||||
<!-- LabelCreationDialog -->
|
||||
<string name="label_creation_title">Create New Label</string>
|
||||
<string name="label_creation_content">Assign a name and color.</string>
|
||||
<string name="label_creation_action_create">Create</string>
|
||||
<string name="label_creation_action_cancel">Cancel</string>
|
||||
<string name="label_creation_label_placeholder">Label Name</string>
|
||||
|
||||
<!-- LabelSelectionSheet -->
|
||||
<string name="label_selection_sheet_title">Filter by Label</string>
|
||||
<string name="label_selection_sheet_title_alt">Set Labels</string>
|
||||
<string name="label_selection_sheet_action_cancel">Cancel</string>
|
||||
<string name="label_selection_sheet_action_search">Search</string>
|
||||
<string name="label_selection_sheet_action_save">Save</string>
|
||||
<string name="label_selection_sheet_text_create">Create a new label named \"%1$s\"</string>
|
||||
|
||||
<!-- LibraryFilterBar -->
|
||||
<string name="library_filter_bar_label_labels">Labels</string>
|
||||
|
||||
<!-- LibraryNavigationBar -->
|
||||
<string name="library_nav_bar_title">Library</string>
|
||||
<string name="library_nav_bar_title_alt"></string>
|
||||
<string name="library_nav_bar_field_placeholder_search">Search</string>
|
||||
|
||||
<!-- LibraryViewModel -->
|
||||
<string name="library_view_model_snackbar_success">Labels updated</string>
|
||||
<string name="library_view_model_snackbar_error">Unable to set labels</string>
|
||||
|
||||
<!-- NotebookView -->
|
||||
<string name="notebook_view_title">Notebook</string>
|
||||
<string name="notebook_view_action_copy">Copy</string>
|
||||
<string name="notebook_view_snackbar_msg">Notebook copied</string>
|
||||
|
||||
<!-- EditNoteModal -->
|
||||
<string name="edit_note_modal_title">Note</string>
|
||||
<string name="edit_note_modal_action_save">Save</string>
|
||||
<string name="edit_note_modal_action_cancel">Cancel</string>
|
||||
|
||||
<!-- ArticleNotes -->
|
||||
<string name="article_notes_title">Article Notes</string>
|
||||
<string name="article_notes_action_add_notes">Add Notes...</string>
|
||||
|
||||
<!-- HighlightsList -->
|
||||
<string name="highlights_list_title">Highlights</string>
|
||||
<string name="highlights_list_action_copy">Copy</string>
|
||||
<string name="highlights_list_snackbar_msg">Highlight copied</string>
|
||||
<string name="highlights_list_action_add_note">Add Note...</string>
|
||||
<string name="highlights_list_error_msg_no_highlights">You have not added any highlights to this page.</string>
|
||||
|
||||
<!-- ReaderPreferencesView -->
|
||||
<string name="reader_preferences_view_font_size">Font Size:</string>
|
||||
<string name="reader_preferences_view_margin">Margin</string>
|
||||
<string name="reader_preferences_view_line_spacing">Line Spacing</string>
|
||||
<string name="reader_preferences_view_theme">Theme:</string>
|
||||
<string name="reader_preferences_view_auto">Auto</string>
|
||||
<string name="reader_preferences_view_high_constrast_text">High Contrast Text</string>
|
||||
<string name="reader_preferences_view_justify_text">Justify Text</string>
|
||||
|
||||
<!-- WebReaderLoadingContainer -->
|
||||
<string name="web_reader_loading_container_error_msg">We were unable to fetch your content.</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_reader_preferences">Reader Preferences</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_notebook">Notebook</string>
|
||||
<string name="web_reader_loading_container_bottom_sheet_open_link">Open Link</string>
|
||||
|
||||
<!-- OpenLinkView -->
|
||||
<string name="open_link_view_action_open_in_browser">Open in Browser</string>
|
||||
<string name="open_link_view_action_save_to_omnivore">Save to Omnivore</string>
|
||||
<string name="open_link_view_action_copy_link">Copy Link</string>
|
||||
<string name="open_link_view_action_cancel">Cancel</string>
|
||||
|
||||
<!-- WebReaderViewModel -->
|
||||
<string name="web_reader_view_model_save_link_success">Link saved</string>
|
||||
<string name="web_reader_view_model_save_link_error">Error saving link</string>
|
||||
<string name="web_reader_view_model_copy_link_success">Link copied</string>
|
||||
|
||||
<!-- SaveContent -->
|
||||
<string name="save_content_msg">Saving</string>
|
||||
<string name="save_content_action_read_now">Read Now</string>
|
||||
<string name="save_content_action_read_later">Read Later</string>
|
||||
<string name="save_content_action_dismiss">Dismiss</string>
|
||||
|
||||
<!-- SaveViewModel -->
|
||||
<string name="save_view_model_msg">Saving to Omnivore...</string>
|
||||
<string name="save_view_model_error_not_logged_in">You are not logged in. Please login before saving.</string>
|
||||
<string name="save_view_model_page_saved_success">Page Saved</string>
|
||||
<string name="save_view_model_page_saved_error">There was an error saving your page</string>
|
||||
|
||||
<!-- SavedItemContextMenu -->
|
||||
<string name="saved_item_context_menu_action_edit_labels">Edit Labels</string>
|
||||
<string name="saved_item_context_menu_action_archive">Archive</string>
|
||||
<string name="saved_item_context_menu_action_unarchive">Unarchive</string>
|
||||
<string name="saved_item_context_menu_action_share_original">Share Original</string>
|
||||
<string name="saved_item_context_menu_action_remove_item">Remove Item</string>
|
||||
|
||||
<!-- LogoutDialog -->
|
||||
<string name="logout_dialog_title">Logout</string>
|
||||
<string name="logout_dialog_confirm_msg">Are you sure you want to logout?</string>
|
||||
<string name="logout_dialog_action_confirm">Confirm</string>
|
||||
<string name="logout_dialog_action_cancel">Cancel</string>
|
||||
|
||||
<!-- ManageAccount -->
|
||||
<string name="manage_account_title">Manage Account</string>
|
||||
<string name="manage_account_action_reset_data_cache">Reset Data Cache</string>
|
||||
|
||||
<!-- PolicyWebView -->
|
||||
<string name="policy_webview_title">Settings</string>
|
||||
|
||||
<!-- SettingsView -->
|
||||
<string name="settings_view_title">Settings</string>
|
||||
<string name="settings_view_setting_row_documentation">Documentation</string>
|
||||
<string name="settings_view_setting_row_feedback">Feedback</string>
|
||||
<string name="settings_view_setting_row_privacy_policy">Privacy Policy</string>
|
||||
<string name="settings_view_setting_row_terms_and_conditions">Terms and Conditions</string>
|
||||
<string name="settings_view_setting_row_manage_account">Manage Account</string>
|
||||
<string name="settings_view_setting_row_logout">Logout</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -44,16 +44,16 @@ import Views
|
|||
NavigationView {
|
||||
// The first column is the sidebar.
|
||||
PrimaryContentSidebar(categories: categories)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
// Second column is the Primary Nav Stack
|
||||
PrimaryContentCategory.feed.destinationView
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
.introspectSplitViewController {
|
||||
$0.preferredSplitBehavior = .tile
|
||||
$0.preferredPrimaryColumnWidth = 160
|
||||
$0.presentsWithGesture = false
|
||||
$0.displayModeButtonVisibility = .always
|
||||
}
|
||||
}
|
||||
|
|
@ -62,26 +62,23 @@ import Views
|
|||
|
||||
@MainActor struct PrimaryContentSidebar: View {
|
||||
@State private var addLinkPresented = false
|
||||
@State private var showProfile = false
|
||||
@State private var selectedCategory: PrimaryContentCategory?
|
||||
let categories: [PrimaryContentCategory]
|
||||
|
||||
var innerBody: some View {
|
||||
List {
|
||||
ForEach(categories, id: \.self) { category in
|
||||
NavigationLink(
|
||||
destination: category.destinationView,
|
||||
tag: category,
|
||||
selection: $selectedCategory,
|
||||
label: { category.listLabel }
|
||||
)
|
||||
#if os(iOS)
|
||||
.listRowBackground(
|
||||
category == selectedCategory
|
||||
? Color.appGraySolid.opacity(0.4).cornerRadius(8)
|
||||
: Color.clear.cornerRadius(8)
|
||||
)
|
||||
#endif
|
||||
}
|
||||
NavigationLink(
|
||||
destination: PrimaryContentCategory.feed.destinationView,
|
||||
tag: PrimaryContentCategory.feed,
|
||||
selection: $selectedCategory,
|
||||
label: { PrimaryContentCategory.feed.listLabel }
|
||||
)
|
||||
.listRowBackground(Color.systemBackground.cornerRadius(8))
|
||||
|
||||
Button(action: { showProfile = true }, label: {
|
||||
PrimaryContentCategory.profile.listLabel
|
||||
})
|
||||
|
||||
Button(action: { addLinkPresented = true }, label: {
|
||||
Label("Add Link", systemImage: "plus.circle")
|
||||
|
|
@ -97,6 +94,14 @@ import Views
|
|||
#endif
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showProfile) {
|
||||
NavigationView {
|
||||
PrimaryContentCategory.profile.destinationView
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ public final class RootViewModel: ObservableObject {
|
|||
EventTracker.registerUser(userID: viewer.unwrappedUserID)
|
||||
}
|
||||
|
||||
services.dataService.cleanupDeletedItems(in: services.dataService.viewContext)
|
||||
|
||||
#if DEBUG
|
||||
if CommandLine.arguments.contains("--uitesting") {
|
||||
services.authenticator.logout(dataService: services.dataService)
|
||||
|
|
|
|||
|
|
@ -63,11 +63,10 @@ public final class DataService: ObservableObject {
|
|||
fatalError("Core Data store failed to load with error: \(error)")
|
||||
}
|
||||
}
|
||||
cleanupDeletedItems(in: viewContext)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupDeletedItems(in context: NSManagedObjectContext) {
|
||||
public func cleanupDeletedItems(in context: NSManagedObjectContext) {
|
||||
let fetchRequest: NSFetchRequest<LinkedItem> = LinkedItem.fetchRequest()
|
||||
|
||||
let calendar = Calendar.current
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import * as httpContext from 'express-http-context2'
|
|||
import * as jwt from 'jsonwebtoken'
|
||||
import { EntityManager } from 'typeorm'
|
||||
import { promisify } from 'util'
|
||||
import { appDataSource } from './data_source'
|
||||
import { sanitizeDirectiveTransformer } from './directives'
|
||||
import { env } from './env'
|
||||
import { createPubSubClient } from './pubsub'
|
||||
import { entityManager } from './repository'
|
||||
import { functionResolvers } from './resolvers/function_resolvers'
|
||||
import { ClaimsToSet, ResolverContext } from './resolvers/types'
|
||||
import ScalarResolvers from './scalars'
|
||||
|
|
@ -79,7 +79,7 @@ const contextFunc: ContextFunction<ExpressContext, ResolverContext> = async ({
|
|||
cb: (em: EntityManager) => TResult,
|
||||
userRole?: string
|
||||
): Promise<TResult> =>
|
||||
entityManager.transaction(async (tx) => {
|
||||
appDataSource.transaction(async (tx) => {
|
||||
await setClaims(tx, undefined, userRole)
|
||||
return cb(tx)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { PubSub } from '@google-cloud/pubsub'
|
||||
import {
|
||||
BaseEntity,
|
||||
EntitySubscriberInterface,
|
||||
EventSubscriber,
|
||||
InsertEvent,
|
||||
} from 'typeorm'
|
||||
import { env } from '../env'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const TOPIC_NAME = 'EntityCreated'
|
||||
|
||||
@EventSubscriber()
|
||||
export class PublishEntitySubscriber implements EntitySubscriberInterface {
|
||||
async afterInsert(event: InsertEvent<BaseEntity>): Promise<void> {
|
||||
const client = new PubSub()
|
||||
|
||||
const msg = JSON.stringify({
|
||||
type: 'EntityCreated',
|
||||
entity: event.entity,
|
||||
entityClass: event.entity?.constructor?.name,
|
||||
})
|
||||
|
||||
if (env.dev.isLocal) {
|
||||
logger.info('PublishEntitySubscriber', msg)
|
||||
return
|
||||
}
|
||||
|
||||
await client
|
||||
.topic(TOPIC_NAME)
|
||||
.publishMessage({ data: Buffer.from(msg) })
|
||||
.catch((err) => {
|
||||
logger.error('PublishEntitySubscriber error publishing event', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
34
packages/api/src/events/user/profile_created.ts
Normal file
34
packages/api/src/events/user/profile_created.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
EntitySubscriberInterface,
|
||||
EventSubscriber,
|
||||
InsertEvent,
|
||||
} from 'typeorm'
|
||||
import { Profile } from '../../entity/profile'
|
||||
import { createDefaultFiltersForUser } from '../../services/create_user'
|
||||
import { addPopularReadsForNewUser } from '../../services/popular_reads'
|
||||
|
||||
@EventSubscriber()
|
||||
export class AddPopularReadsToNewUser
|
||||
implements EntitySubscriberInterface<Profile>
|
||||
{
|
||||
listenTo() {
|
||||
return Profile
|
||||
}
|
||||
|
||||
async afterInsert(event: InsertEvent<Profile>): Promise<void> {
|
||||
await addPopularReadsForNewUser(event.entity.user.id, event.manager)
|
||||
}
|
||||
}
|
||||
|
||||
@EventSubscriber()
|
||||
export class AddDefaultFiltersToNewUser
|
||||
implements EntitySubscriberInterface<Profile>
|
||||
{
|
||||
listenTo() {
|
||||
return Profile
|
||||
}
|
||||
|
||||
async afterInsert(event: InsertEvent<Profile>): Promise<void> {
|
||||
await createDefaultFiltersForUser(event.manager)(event.entity.user.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import {
|
||||
EntitySubscriberInterface,
|
||||
EventSubscriber,
|
||||
InsertEvent,
|
||||
} from 'typeorm'
|
||||
import { Profile } from '../../entity/profile'
|
||||
import { createPubSubClient } from '../../pubsub'
|
||||
import { addPopularReadsForNewUser } from '../../services/popular_reads'
|
||||
import { IntercomClient } from '../../utils/intercom'
|
||||
|
||||
@EventSubscriber()
|
||||
export class CreateIntercomAccount
|
||||
implements EntitySubscriberInterface<Profile>
|
||||
{
|
||||
listenTo() {
|
||||
return Profile
|
||||
}
|
||||
|
||||
async afterInsert(event: InsertEvent<Profile>): Promise<void> {
|
||||
const profile = event.entity
|
||||
|
||||
const customAttributes: { source_user_id: string } = {
|
||||
source_user_id: profile.user.sourceUserId,
|
||||
}
|
||||
await IntercomClient?.contacts.createUser({
|
||||
email: profile.user.email,
|
||||
externalId: profile.user.id,
|
||||
name: profile.user.name,
|
||||
avatar: profile.pictureUrl || undefined,
|
||||
customAttributes: customAttributes,
|
||||
signedUpAt: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@EventSubscriber()
|
||||
export class PublishNewUserEvent implements EntitySubscriberInterface<Profile> {
|
||||
listenTo() {
|
||||
return Profile
|
||||
}
|
||||
|
||||
async afterInsert(event: InsertEvent<Profile>): Promise<void> {
|
||||
const client = createPubSubClient()
|
||||
await client.userCreated(
|
||||
event.entity.user.id,
|
||||
event.entity.user.email,
|
||||
event.entity.user.name,
|
||||
event.entity.username
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@EventSubscriber()
|
||||
export class AddPopularReadsToNewUser
|
||||
implements EntitySubscriberInterface<Profile>
|
||||
{
|
||||
listenTo() {
|
||||
return Profile
|
||||
}
|
||||
|
||||
async afterInsert(event: InsertEvent<Profile>): Promise<void> {
|
||||
await addPopularReadsForNewUser(event.entity.user.id, event.manager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { DeepPartial } from 'typeorm'
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
|
||||
import { entityManager } from '.'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { unescapeHtml } from '../utils/helpers'
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ const unescapeHighlight = (highlight: DeepPartial<Highlight>) => {
|
|||
return highlight
|
||||
}
|
||||
|
||||
export const highlightRepository = entityManager
|
||||
export const highlightRepository = appDataSource
|
||||
.getRepository(Highlight)
|
||||
.extend({
|
||||
findById(id: string) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const setClaims = async (
|
|||
|
||||
export const authTrx = async <T>(
|
||||
fn: (manager: EntityManager) => Promise<T>,
|
||||
em = entityManager,
|
||||
em = appDataSource.manager,
|
||||
uid?: string,
|
||||
userRole?: string
|
||||
): Promise<T> => {
|
||||
|
|
@ -40,7 +40,5 @@ export const authTrx = async <T>(
|
|||
}
|
||||
|
||||
export const getRepository = <T>(entity: EntityTarget<T>) => {
|
||||
return entityManager.getRepository(entity)
|
||||
return appDataSource.getRepository(entity)
|
||||
}
|
||||
|
||||
export const entityManager = appDataSource.manager
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { In } from 'typeorm'
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
|
||||
import { entityManager } from '.'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Label } from '../entity/label'
|
||||
import { generateRandomColor } from '../utils/helpers'
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ const convertToLabel = (label: CreateLabelInput, userId: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const labelRepository = entityManager.getRepository(Label).extend({
|
||||
export const labelRepository = appDataSource.getRepository(Label).extend({
|
||||
findById(id: string) {
|
||||
return this.findOneBy({ id })
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { entityManager } from '.'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
|
||||
export const libraryItemRepository = entityManager
|
||||
export const libraryItemRepository = appDataSource
|
||||
.getRepository(LibraryItem)
|
||||
.extend({
|
||||
findById(id: string) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { In } from 'typeorm'
|
||||
import { entityManager } from '.'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { User } from './../entity/user'
|
||||
|
||||
const TOP_USERS = [
|
||||
|
|
@ -14,7 +14,7 @@ const TOP_USERS = [
|
|||
]
|
||||
export const MAX_RECORDS_LIMIT = 1000
|
||||
|
||||
export const userRepository = entityManager.getRepository(User).extend({
|
||||
export const userRepository = appDataSource.getRepository(User).extend({
|
||||
findById(id: string) {
|
||||
return this.findOneBy({ id })
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,29 +1,22 @@
|
|||
import { EntityManager } from 'typeorm'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Filter } from '../entity/filter'
|
||||
import { GroupMembership } from '../entity/groups/group_membership'
|
||||
import { Invite } from '../entity/groups/invite'
|
||||
import { Profile } from '../entity/profile'
|
||||
import { StatusType, User } from '../entity/user'
|
||||
import { env } from '../env'
|
||||
import { SignupErrorCode } from '../generated/graphql'
|
||||
import { authTrx, entityManager, getRepository } from '../repository'
|
||||
import { createPubSubClient } from '../pubsub'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
import { userRepository } from '../repository/user'
|
||||
import { AuthProvider } from '../routers/auth/auth_types'
|
||||
import { analytics } from '../utils/analytics'
|
||||
import { IntercomClient } from '../utils/intercom'
|
||||
import { logger } from '../utils/logger'
|
||||
import { validateUsername } from '../utils/usernamePolicy'
|
||||
import { sendConfirmationEmail } from './send_emails'
|
||||
import { Filter } from '../entity/filter'
|
||||
import { analytics } from '../utils/analytics'
|
||||
import { env } from '../env'
|
||||
|
||||
const TOP_USERS = [
|
||||
'jacksonh',
|
||||
'nat',
|
||||
'luis',
|
||||
'satindar',
|
||||
'malandrina',
|
||||
'patrick',
|
||||
'alexgutjahr',
|
||||
'hongbowu',
|
||||
]
|
||||
export const MAX_RECORDS_LIMIT = 1000
|
||||
|
||||
export const createUser = async (input: {
|
||||
|
|
@ -71,7 +64,7 @@ export const createUser = async (input: {
|
|||
return Promise.reject({ errorCode: SignupErrorCode.InvalidUsername })
|
||||
}
|
||||
|
||||
const [user, profile] = await entityManager.transaction<[User, Profile]>(
|
||||
const [user, profile] = await appDataSource.transaction<[User, Profile]>(
|
||||
async (t) => {
|
||||
let hasInvite = false
|
||||
let invite: Invite | null = null
|
||||
|
|
@ -110,17 +103,29 @@ export const createUser = async (input: {
|
|||
})
|
||||
}
|
||||
|
||||
await createDefaultFiltersForUser(t)(user.id)
|
||||
|
||||
return [user, profile]
|
||||
}
|
||||
)
|
||||
|
||||
if (input.pendingConfirmation) {
|
||||
if (!(await sendConfirmationEmail(user))) {
|
||||
return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail })
|
||||
}
|
||||
const customAttributes: { source_user_id: string } = {
|
||||
source_user_id: user.sourceUserId,
|
||||
}
|
||||
await IntercomClient?.contacts.createUser({
|
||||
email: user.email,
|
||||
externalId: user.id,
|
||||
name: user.name,
|
||||
avatar: profile.pictureUrl || undefined,
|
||||
customAttributes: customAttributes,
|
||||
signedUpAt: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
|
||||
const pubsubClient = createPubSubClient()
|
||||
await pubsubClient.userCreated(
|
||||
user.id,
|
||||
user.email,
|
||||
user.name,
|
||||
profile.username
|
||||
)
|
||||
|
||||
analytics.track({
|
||||
userId: user.id,
|
||||
|
|
@ -132,10 +137,16 @@ export const createUser = async (input: {
|
|||
},
|
||||
})
|
||||
|
||||
if (input.pendingConfirmation) {
|
||||
if (!(await sendConfirmationEmail(user))) {
|
||||
return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail })
|
||||
}
|
||||
}
|
||||
|
||||
return [user, profile]
|
||||
}
|
||||
|
||||
const createDefaultFiltersForUser =
|
||||
export const createDefaultFiltersForUser =
|
||||
(t: EntityManager) =>
|
||||
async (userId: string): Promise<Filter[]> => {
|
||||
const defaultFilters = [
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import * as jwt from 'jsonwebtoken'
|
||||
import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Feature } from '../entity/feature'
|
||||
import { env } from '../env'
|
||||
import { entityManager, getRepository } from '../repository'
|
||||
import { getRepository } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
export enum FeatureName {
|
||||
|
|
@ -41,7 +42,7 @@ const optInUltraRealisticVoice = async (uid: string): Promise<Feature> => {
|
|||
|
||||
const MAX_USERS = 1500
|
||||
// opt in to feature for the first 1500 users
|
||||
const optedInFeatures = (await entityManager.query(
|
||||
const optedInFeatures = (await appDataSource.query(
|
||||
`insert into omnivore.features (user_id, name, granted_at)
|
||||
select $1, $2, $3 from omnivore.features
|
||||
where name = $2 and granted_at is not null
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { nanoid } from 'nanoid'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Group } from '../entity/groups/group'
|
||||
import { GroupMembership } from '../entity/groups/group_membership'
|
||||
import { Invite } from '../entity/groups/invite'
|
||||
|
|
@ -6,7 +7,7 @@ import { RuleActionType } from '../entity/rule'
|
|||
import { User } from '../entity/user'
|
||||
import { homePageURL } from '../env'
|
||||
import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql'
|
||||
import { entityManager, getRepository } from '../repository'
|
||||
import { getRepository } from '../repository'
|
||||
import { userDataToUser } from '../utils/helpers'
|
||||
import { findOrCreateLabels } from './labels'
|
||||
import { createRule } from './rules'
|
||||
|
|
@ -21,7 +22,7 @@ export const createGroup = async (input: {
|
|||
onlyAdminCanPost?: boolean | null
|
||||
onlyAdminCanSeeMembers?: boolean | null
|
||||
}): Promise<[Group, Invite]> => {
|
||||
const [group, invite] = await entityManager.transaction<[Group, Invite]>(
|
||||
const [group, invite] = await appDataSource.transaction<[Group, Invite]>(
|
||||
async (t) => {
|
||||
// Max number of groups a user can create
|
||||
const maxGroups = 3
|
||||
|
|
@ -113,7 +114,7 @@ export const joinGroup = async (
|
|||
user: User,
|
||||
inviteCode: string
|
||||
): Promise<RecommendationGroup> => {
|
||||
const invite = await entityManager.transaction<Invite>(async (t) => {
|
||||
const invite = await appDataSource.transaction<Invite>(async (t) => {
|
||||
// Check if the invite exists
|
||||
const invite = await t
|
||||
.getRepository(Invite)
|
||||
|
|
@ -173,7 +174,7 @@ export const leaveGroup = async (
|
|||
user: User,
|
||||
groupId: string
|
||||
): Promise<boolean> => {
|
||||
return entityManager.transaction(async (t) => {
|
||||
return appDataSource.transaction(async (t) => {
|
||||
const group = await t
|
||||
.getRepository(Group)
|
||||
.createQueryBuilder('group')
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import * as httpContext from 'express-http-context2'
|
|||
import { readFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { DeepPartial, EntityManager } from 'typeorm'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
import { PageType } from '../generated/graphql'
|
||||
import { authTrx, entityManager } from '../repository'
|
||||
import { authTrx } from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { generateSlug, stringToHash, wordsCount } from '../utils/helpers'
|
||||
import { logger } from '../utils/logger'
|
||||
|
|
@ -107,7 +108,7 @@ const addPopularReads = async (
|
|||
|
||||
export const addPopularReadsForNewUser = async (
|
||||
userId: string,
|
||||
em = entityManager
|
||||
em = appDataSource.manager
|
||||
): Promise<void> => {
|
||||
const defaultReads = ['omnivore_organize', 'power_read_it_later']
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { AbuseReport } from '../entity/reports/abuse_report'
|
||||
import { ContentDisplayReport } from '../entity/reports/content_display_report'
|
||||
import { env } from '../env'
|
||||
import { ReportItemInput, ReportType } from '../generated/graphql'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
import { sendEmail } from '../utils/sendEmail'
|
||||
import { findLibraryItemById } from './library_item'
|
||||
|
||||
export const saveContentDisplayReport = async (
|
||||
|
|
@ -18,7 +20,7 @@ export const saveContentDisplayReport = async (
|
|||
// We capture the article content and original html now, in case it
|
||||
// reparsed or updated later, this gives us a view of exactly
|
||||
// what the user saw.
|
||||
const result = await getRepository(ContentDisplayReport).save({
|
||||
const report = await getRepository(ContentDisplayReport).save({
|
||||
user: { id: uid },
|
||||
content: item.readableContent,
|
||||
originalHtml: item.originalContent || undefined,
|
||||
|
|
@ -27,7 +29,23 @@ export const saveContentDisplayReport = async (
|
|||
libraryItemId: item.id,
|
||||
})
|
||||
|
||||
return !!result
|
||||
const message = `A new content display report was created by:
|
||||
${report.user.id} for URL: ${report.originalUrl}
|
||||
${report.reportComment}`
|
||||
|
||||
logger.info(message)
|
||||
|
||||
if (!env.dev.isLocal) {
|
||||
// If we are in the local environment, just log a message, otherwise email the report
|
||||
await sendEmail({
|
||||
to: env.sender.feedback,
|
||||
subject: 'New content display report',
|
||||
text: message,
|
||||
from: env.sender.message,
|
||||
})
|
||||
}
|
||||
|
||||
return !!report
|
||||
}
|
||||
|
||||
export const saveAbuseReport = async (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import axios from 'axios'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { NewsletterEmail } from '../entity/newsletter_email'
|
||||
import { Subscription } from '../entity/subscription'
|
||||
import { SubscriptionStatus, SubscriptionType } from '../generated/graphql'
|
||||
import { authTrx, entityManager, getRepository } from '../repository'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
import { sendEmail } from '../utils/sendEmail'
|
||||
|
||||
|
|
@ -105,7 +106,7 @@ export const saveSubscription = async ({
|
|||
}
|
||||
|
||||
const existingSubscription = await getSubscriptionByName(name, userId)
|
||||
const result = await entityManager.transaction(async (tx) => {
|
||||
const result = await appDataSource.transaction(async (tx) => {
|
||||
if (existingSubscription) {
|
||||
// update subscription if already exists
|
||||
await tx
|
||||
|
|
|
|||
|
|
@ -74,11 +74,6 @@ interface BackendEnv {
|
|||
gcsUploadSAKeyFilePath: string
|
||||
gcsUploadPrivateBucket: string
|
||||
}
|
||||
elastic: {
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
sender: {
|
||||
message: string
|
||||
feedback: string
|
||||
|
|
@ -144,8 +139,6 @@ const nullableEnvVars = [
|
|||
'GAUTH_SECRET',
|
||||
'SEGMENT_WRITE_KEY',
|
||||
'TWITTER_BEARER_TOKEN',
|
||||
'ELASTIC_USERNAME',
|
||||
'ELASTIC_PASSWORD',
|
||||
'GCS_UPLOAD_PRIVATE_BUCKET',
|
||||
'SENDER_MESSAGE',
|
||||
'SENDER_FEEDBACK',
|
||||
|
|
@ -267,11 +260,6 @@ export function getEnv(): BackendEnv {
|
|||
gcsUploadSAKeyFilePath: parse('GCS_UPLOAD_SA_KEY_FILE_PATH'),
|
||||
gcsUploadPrivateBucket: parse('GCS_UPLOAD_PRIVATE_BUCKET'),
|
||||
}
|
||||
const elastic = {
|
||||
url: parse('ELASTIC_URL'),
|
||||
username: parse('ELASTIC_USERNAME'),
|
||||
password: parse('ELASTIC_PASSWORD'),
|
||||
}
|
||||
const sender = {
|
||||
message: parse('SENDER_MESSAGE'),
|
||||
feedback: parse('SENDER_FEEDBACK'),
|
||||
|
|
@ -317,7 +305,6 @@ export function getEnv(): BackendEnv {
|
|||
dev,
|
||||
fileUpload,
|
||||
queue,
|
||||
elastic,
|
||||
sender,
|
||||
sendgrid,
|
||||
readwise,
|
||||
|
|
|
|||
|
|
@ -260,15 +260,17 @@ export const enqueueParseRequest = async ({
|
|||
|
||||
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
|
||||
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios.post(env.queue.contentFetchUrl, payload).catch((error) => {
|
||||
logError(error)
|
||||
logger.error(
|
||||
`Error occurred while requesting local puppeteer-parse function\nPlease, ensure your function is set up properly and running using "yarn start" from the "/pkg/gcf/puppeteer-parse" folder`
|
||||
)
|
||||
})
|
||||
}, 0)
|
||||
if (env.queue.contentFetchUrl) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios.post(env.queue.contentFetchUrl, payload).catch((error) => {
|
||||
logError(error)
|
||||
logger.error(
|
||||
`Error occurred while requesting local puppeteer-parse function\nPlease, ensure your function is set up properly and running using "yarn start" from the "/pkg/gcf/puppeteer-parse" folder`
|
||||
)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
|
@ -414,12 +416,14 @@ export const enqueueTextToSpeech = async ({
|
|||
const taskHandlerUrl = `${env.queue.textToSpeechTaskHandlerUrl}?token=${token}`
|
||||
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
|
||||
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios.post(taskHandlerUrl, payload).catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
if (env.queue.textToSpeechTaskHandlerUrl) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios.post(taskHandlerUrl, payload).catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const createdTasks = await createHttpTaskWithToken({
|
||||
|
|
@ -461,16 +465,18 @@ export const enqueueRecommendation = async (
|
|||
}
|
||||
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
|
||||
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(env.queue.recommendationTaskHandlerUrl, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
if (env.queue.recommendationTaskHandlerUrl) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(env.queue.recommendationTaskHandlerUrl, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
|
@ -505,16 +511,18 @@ export const enqueueImportFromIntegration = async (
|
|||
}
|
||||
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
|
||||
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(`${env.queue.integrationTaskHandlerUrl}/import`, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
if (env.queue.integrationTaskHandlerUrl) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(`${env.queue.integrationTaskHandlerUrl}/import`, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
return nanoid()
|
||||
}
|
||||
|
||||
|
|
@ -552,16 +560,18 @@ export const enqueueThumbnailTask = async (
|
|||
|
||||
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
|
||||
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(env.queue.thumbnailTaskHandlerUrl, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
if (env.queue.thumbnailTaskHandlerUrl) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(env.queue.thumbnailTaskHandlerUrl, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
|
@ -599,16 +609,18 @@ export const enqueueRssFeedFetch = async (
|
|||
|
||||
// If there is no Google Cloud Project Id exposed, it means that we are in local environment
|
||||
if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(env.queue.rssFeedTaskHandlerUrl, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
if (env.queue.rssFeedTaskHandlerUrl) {
|
||||
// Calling the handler function directly.
|
||||
setTimeout(() => {
|
||||
axios
|
||||
.post(env.queue.rssFeedTaskHandlerUrl, payload, {
|
||||
headers,
|
||||
})
|
||||
.catch((error) => {
|
||||
logError(error)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
return nanoid()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ const getJSONLdLinkMetadata = async (
|
|||
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error(`Unable to get JSONLD link of the article`, error)
|
||||
logger.error('Unable to get JSONLD link of the article')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const sendEmail = async (msg: MailDataRequired): Promise<boolean> => {
|
|||
const client = new MailService()
|
||||
if (!process.env.SENDGRID_MSGS_API_KEY) {
|
||||
if (env.dev.isLocal) {
|
||||
logger.error('SendGrid API key not set.\nSending email:', msg)
|
||||
logger.info('SendGrid API key not set.\nSending email:', msg)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { DeepPartial } from 'typeorm'
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'
|
||||
import { appDataSource } from '../src/data_source'
|
||||
import { Filter } from '../src/entity/filter'
|
||||
import { Label } from '../src/entity/label'
|
||||
|
|
@ -7,7 +6,7 @@ import { LibraryItem } from '../src/entity/library_item'
|
|||
import { Reminder } from '../src/entity/reminder'
|
||||
import { User } from '../src/entity/user'
|
||||
import { UserDeviceToken } from '../src/entity/user_device_tokens'
|
||||
import { entityManager, getRepository, setClaims } from '../src/repository'
|
||||
import { getRepository, setClaims } from '../src/repository'
|
||||
import { userRepository } from '../src/repository/user'
|
||||
import { createUser } from '../src/services/create_user'
|
||||
import { saveLabelsInLibraryItem } from '../src/services/labels'
|
||||
|
|
@ -27,13 +26,18 @@ export const createTestConnection = async (): Promise<void> => {
|
|||
logging: ['query', 'info'],
|
||||
entities: [__dirname + '/../src/entity/**/*{.js,.ts}'],
|
||||
subscribers: [__dirname + '/../src/events/**/*{.js,.ts}'],
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
logger: process.env.PG_LOGGER as
|
||||
| 'advanced-console'
|
||||
| 'simple-console'
|
||||
| 'file'
|
||||
| 'debug'
|
||||
| undefined,
|
||||
})
|
||||
await appDataSource.initialize()
|
||||
}
|
||||
|
||||
export const deleteFiltersFromUser = async (userId: string) => {
|
||||
await entityManager.transaction(async (t) => {
|
||||
await appDataSource.transaction(async (t) => {
|
||||
await setClaims(t, userId)
|
||||
const filterRepo = t.getRepository(Filter)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ describe('Webhooks API', () => {
|
|||
}
|
||||
`
|
||||
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.webhook.webhook.id).to.eql(webhook.id)
|
||||
expect(res.body.data.webhook.webhook.url).to.eql(webhook.url)
|
||||
|
|
@ -108,7 +108,7 @@ describe('Webhooks API', () => {
|
|||
}
|
||||
`
|
||||
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
const webhooks = await findWebhooks(user.id)
|
||||
|
||||
expect(res.body.data.webhooks.webhooks).to.eql(
|
||||
|
|
@ -165,7 +165,7 @@ describe('Webhooks API', () => {
|
|||
})
|
||||
|
||||
it('should create a webhook', async () => {
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.setWebhook.webhook).to.be.an('object')
|
||||
expect(res.body.data.setWebhook.webhook.url).to.eql(webhookUrl)
|
||||
|
|
@ -195,7 +195,7 @@ describe('Webhooks API', () => {
|
|||
})
|
||||
|
||||
it('should update a webhook', async () => {
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.setWebhook.webhook).to.be.an('object')
|
||||
expect(res.body.data.setWebhook.webhook.url).to.eql(webhookUrl)
|
||||
|
|
@ -240,7 +240,7 @@ describe('Webhooks API', () => {
|
|||
})
|
||||
|
||||
it('should delete a webhook', async () => {
|
||||
const res = await graphqlRequest(query, authToken)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
const webhook = await findWebhookById(webhookId, user.id)
|
||||
|
||||
expect(res.body.data.deleteWebhook.webhook).to.be.an('object')
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ describe('/article/save API', () => {
|
|||
// We need to mock the pupeeteer-parse
|
||||
// service here because in dev mode the task gets
|
||||
// called immediately.
|
||||
nock(env.queue.contentFetchUrl).post('/').reply(200)
|
||||
if (env.queue.contentFetchUrl) {
|
||||
nock(env.queue.contentFetchUrl).post('/').reply(200)
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
// create test user and login
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { createHighlightMutation } from '../../../lib/networking/mutations/creat
|
|||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
import { pspdfKitKey } from '../../../lib/appConfig'
|
||||
import { HighlightNoteModal } from './HighlightNoteModal'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
|
|
@ -22,7 +21,6 @@ import 'react-sliding-pane/dist/react-sliding-pane.css'
|
|||
import { NotebookContent } from './Notebook'
|
||||
import { NotebookHeader } from './NotebookHeader'
|
||||
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
|
||||
export type PdfArticleContainerProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -419,6 +417,65 @@ export default function PdfArticleContainer(
|
|||
})
|
||||
}
|
||||
)
|
||||
|
||||
function keyDownHandler(event: globalThis.KeyboardEvent) {
|
||||
const key = event.key.toLowerCase()
|
||||
switch (key) {
|
||||
case 'o':
|
||||
document.dispatchEvent(new Event('openOriginalArticle'))
|
||||
break
|
||||
case 'u':
|
||||
const query = window.sessionStorage.getItem('q')
|
||||
if (query) {
|
||||
window.location.assign(`/home?${query}`)
|
||||
} else {
|
||||
window.location.replace(`/home`)
|
||||
}
|
||||
break
|
||||
case 'e':
|
||||
document.dispatchEvent(new Event('archive'))
|
||||
break
|
||||
case '#':
|
||||
document.dispatchEvent(new Event('delete'))
|
||||
break
|
||||
case 'h':
|
||||
const root = (event.target as HTMLElement).querySelector(
|
||||
'.PSPDFKit-Root'
|
||||
)
|
||||
const highlight = root?.querySelector(
|
||||
'.PSPDFKit-Text-Markup-Inline-Toolbar-Highlight'
|
||||
)
|
||||
console.log('root ', root)
|
||||
console.log('highlight overlay: ', highlight, highlight?.nodeName)
|
||||
if (highlight && highlight?.nodeName == 'BUTTON') {
|
||||
const button = highlight as HTMLButtonElement
|
||||
button.click()
|
||||
}
|
||||
break
|
||||
// case 'n':
|
||||
// TODO: need to set a post creation event here, then
|
||||
// go through the regular highlight creation
|
||||
// document.dispatchEvent(new Event('annotate'))
|
||||
// break
|
||||
case 't':
|
||||
props.setShowHighlightsModal(true)
|
||||
break
|
||||
case 'i':
|
||||
document.dispatchEvent(new Event('showEditModal'))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const isIE11 = navigator.userAgent.indexOf('Trident/') > -1
|
||||
instance.contentDocument.addEventListener(
|
||||
'keydown',
|
||||
keyDownHandler,
|
||||
isIE11
|
||||
? {
|
||||
capture: true,
|
||||
}
|
||||
: true
|
||||
)
|
||||
})()
|
||||
|
||||
document.addEventListener('deleteHighlightbyId', async (event) => {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
if (!subscriptionsLoading) {
|
||||
setSubscriptions(networkSubscriptions)
|
||||
}
|
||||
}, [setSubscriptions, networkLabels, subscriptionsLoading])
|
||||
}, [setSubscriptions, networkSubscriptions, subscriptionsLoading])
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchesLoading) {
|
||||
|
|
@ -204,9 +204,9 @@ function Subscriptions(
|
|||
if (!props.subscriptions) {
|
||||
return []
|
||||
}
|
||||
return props.subscriptions.sort((a, b) =>
|
||||
b.updatedAt.localeCompare(a.updatedAt)
|
||||
)
|
||||
return props.subscriptions
|
||||
.filter((s) => s.status == 'ACTIVE')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [props.subscriptions])
|
||||
|
||||
useRegisterActions(
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import {
|
|||
} from './../../../components/templates/article/ArticleContainer'
|
||||
import { PdfArticleContainerProps } from './../../../components/templates/article/PdfArticleContainer'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { navigationCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
|
|
@ -38,7 +36,6 @@ import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
|||
import { SkeletonArticleContainer } from '../../../components/templates/article/SkeletonArticleContainer'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { deleteLinkMutation } from '../../../lib/networking/mutations/deleteLinkMutation'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
import { ReaderHeader } from '../../../components/templates/reader/ReaderHeader'
|
||||
import { EditArticleModal } from '../../../components/templates/homeFeed/EditItemModals'
|
||||
import { VerticalArticleActionsMenu } from '../../../components/templates/article/VerticalArticleActions'
|
||||
|
|
@ -120,6 +117,9 @@ export default function Home(): JSX.Element {
|
|||
})
|
||||
} else {
|
||||
router.push(`/home`)
|
||||
showSuccessToast('Page archived', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -187,16 +187,22 @@ export default function Home(): JSX.Element {
|
|||
actionHandler('mark-read')
|
||||
}
|
||||
|
||||
const showEditModal = () => {
|
||||
actionHandler('showEditModal')
|
||||
}
|
||||
|
||||
document.addEventListener('archive', archive)
|
||||
document.addEventListener('delete', deletePage)
|
||||
document.addEventListener('mark-read', markRead)
|
||||
document.addEventListener('openOriginalArticle', openOriginalArticle)
|
||||
document.addEventListener('showEditModal', showEditModal)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('archive', archive)
|
||||
document.removeEventListener('mark-read', markRead)
|
||||
document.removeEventListener('delete', deletePage)
|
||||
document.removeEventListener('openOriginalArticle', openOriginalArticle)
|
||||
document.removeEventListener('showEditModal', showEditModal)
|
||||
}
|
||||
}, [actionHandler])
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export function PrimaryContent(props: PrimaryContentProps): JSX.Element {
|
|||
router.replace('/login')
|
||||
}
|
||||
|
||||
if (timedOut || error) {
|
||||
if (timedOut) {
|
||||
return (
|
||||
<ErrorComponent errorMessage="Something went wrong while processing the link, please try again in a moment" />
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { FloppyDisk, Pencil, XCircle } from 'phosphor-react'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { FormInput } from '../../../components/elements/FormElements'
|
||||
import { HStack, SpanBox } from '../../../components/elements/LayoutPrimitives'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
|
|
@ -33,6 +33,15 @@ export default function Rss(): JSX.Element {
|
|||
const [onPauseId, setOnPauseId] = useState('')
|
||||
const [onEditStatus, setOnEditStatus] = useState<SubscriptionStatus>()
|
||||
|
||||
const sortedSubscriptions = useMemo(() => {
|
||||
if (!subscriptions) {
|
||||
return []
|
||||
}
|
||||
return subscriptions
|
||||
.filter((s) => s.status == 'ACTIVE')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [subscriptions])
|
||||
|
||||
async function updateSubscription(): Promise<void> {
|
||||
const result = await updateSubscriptionMutation({
|
||||
id: onEditId,
|
||||
|
|
@ -107,10 +116,10 @@ export default function Rss(): JSX.Element {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{subscriptions.length === 0 ? (
|
||||
{sortedSubscriptions.length === 0 ? (
|
||||
<EmptySettingsRow text={isValidating ? '-' : 'No feeds subscribed'} />
|
||||
) : (
|
||||
subscriptions.map((subscription, i) => {
|
||||
sortedSubscriptions.map((subscription, i) => {
|
||||
return (
|
||||
<SettingsTableRow
|
||||
key={subscription.id}
|
||||
|
|
@ -179,7 +188,7 @@ export default function Rss(): JSX.Element {
|
|||
</HStack>
|
||||
)
|
||||
}
|
||||
isLast={i === subscriptions.length - 1}
|
||||
isLast={i === sortedSubscriptions.length - 1}
|
||||
onDelete={() => {
|
||||
console.log('onDelete triggered: ', subscription.id)
|
||||
setOnDeleteId(subscription.id)
|
||||
|
|
@ -190,8 +199,8 @@ export default function Rss(): JSX.Element {
|
|||
)
|
||||
setOnPauseId(subscription.id)
|
||||
}}
|
||||
deleteTitle="Delete"
|
||||
editTitle={subscription.status === 'ACTIVE' ? 'Pause' : 'Resume'}
|
||||
deleteTitle="Unsubscribe"
|
||||
// editTitle={subscription.status === 'ACTIVE' ? 'Pause' : 'Resume'}
|
||||
sublineElement={
|
||||
<SpanBox
|
||||
css={{
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ export default function SubscriptionsPage(): JSX.Element {
|
|||
if (!subscriptions) {
|
||||
return []
|
||||
}
|
||||
return subscriptions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
return subscriptions
|
||||
.filter((s) => s.status == 'ACTIVE')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [subscriptions])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
'use strict'
|
||||
;(function () {
|
||||
const globalApi = (typeof globalThis !== 'undefined' && globalThis) || self
|
||||
const naviApi = globalApi.navigator
|
||||
|
||||
const mainOrigin = 'https://omnivore.app'
|
||||
const devOrigin = 'https://dev.omnivore.app'
|
||||
|
|
@ -27,41 +26,6 @@
|
|||
)
|
||||
return
|
||||
|
||||
const cacheVersion = 'v1.0.0'
|
||||
|
||||
const homeCache = '/?cid=' + cacheVersion
|
||||
|
||||
const initialCacheItems = [
|
||||
homeCache,
|
||||
'/manifest.webmanifest',
|
||||
'/pwa-36.png',
|
||||
'/pwa-48.png',
|
||||
'/pwa-72.png',
|
||||
'/pwa-96.png',
|
||||
'/pwa-144.png',
|
||||
'/pwa-192.png',
|
||||
'/pwa-256.png',
|
||||
'/pwa-384.png',
|
||||
'/pwa-512.png',
|
||||
]
|
||||
|
||||
function fetchWithCacheBackup(request) {
|
||||
return globalApi.fetch(request).then((freshResult) => {
|
||||
if (freshResult.status > 199 && freshResult.status < 400)
|
||||
return freshResult
|
||||
|
||||
return globalApi.caches
|
||||
.match(request, {
|
||||
ignoreSearch: true,
|
||||
ignoreMethod: true,
|
||||
ignoreVary: true,
|
||||
})
|
||||
.then((cachedResult) => {
|
||||
return cachedResult || freshResult
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function findShareUrlInText(formData) {
|
||||
const url = formData.get('url') || ''
|
||||
const text = formData.get('text') || ''
|
||||
|
|
@ -78,88 +42,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function saveArticleUrl(url) {
|
||||
if (!url) {
|
||||
return Promise.reject(new Error('No URL'))
|
||||
}
|
||||
|
||||
const requestUrl = currentOrigin + '/api/article/save'
|
||||
|
||||
return globalApi
|
||||
.fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
url: url,
|
||||
v: '0.2.18',
|
||||
}),
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status === 200) {
|
||||
return response.json().then((responseJson) => {
|
||||
return currentOrigin + '/article?url=' + url
|
||||
})
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
return response.json().then((responseJson) => {
|
||||
if (responseJson.errorCode === 'UNAUTHORIZED') {
|
||||
return currentOrigin + '/login'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleShareTarget(request) {
|
||||
return request
|
||||
.formData()
|
||||
.then((formData) => {
|
||||
const shareUrl = findShareUrlInText(formData)
|
||||
|
||||
return saveArticleUrl(shareUrl).catch(() => {
|
||||
// generic error redirect
|
||||
return currentOrigin + '/'
|
||||
})
|
||||
})
|
||||
.then((responseUrl) => {
|
||||
return Response.redirect(responseUrl, 303)
|
||||
})
|
||||
}
|
||||
|
||||
function handleOutdatedCache() {
|
||||
return globalApi.caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== cacheVersion) {
|
||||
return globalApi.caches.delete(cacheName)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function initCache() {
|
||||
return globalApi.caches.open(cacheVersion).then((cache) => {
|
||||
if (!cache.addAll || !naviApi.onLine) return
|
||||
return cache.addAll(initialCacheItems)
|
||||
})
|
||||
}
|
||||
|
||||
globalApi.addEventListener('install', (ev) => {
|
||||
globalApi.skipWaiting()
|
||||
|
||||
const handler = initCache()
|
||||
ev.waitUntil(handler)
|
||||
})
|
||||
|
||||
globalApi.addEventListener('activate', (ev) => {
|
||||
const handler = handleOutdatedCache()
|
||||
ev.waitUntil(handler)
|
||||
})
|
||||
|
||||
globalApi.addEventListener('fetch', (ev) => {
|
||||
if (ev.request.destination === 'script') {
|
||||
return
|
||||
|
|
@ -168,12 +50,19 @@
|
|||
return
|
||||
}
|
||||
|
||||
if (ev.request.method === 'POST') {
|
||||
const requestUrl = new URL(ev.request.url)
|
||||
if (requestUrl.pathname === '/share-target') {
|
||||
const shareRequest = handleShareTarget(ev.request)
|
||||
return shareRequest
|
||||
}
|
||||
if (
|
||||
ev.request.method === 'POST' &&
|
||||
ev.request.url.endsWith('/share-target')
|
||||
) {
|
||||
ev.respondWith(
|
||||
(async () => {
|
||||
const formData = await ev.request.formData()
|
||||
const sharedUrl = findShareUrlInText(formData)
|
||||
return Response.redirect(`/api/save?url=${sharedUrl}`, 303)
|
||||
})()
|
||||
)
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
console.log('activated service worker')
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@
|
|||
case 'success':
|
||||
// Auto hide if everything went well and the user
|
||||
// has not initiated any interaction.
|
||||
|
||||
hideToastTimeout = setTimeout(function () {
|
||||
console.log('hiding: ', currentToastEl, doNotHide)
|
||||
if (!doNotHide) {
|
||||
|
|
@ -253,6 +254,12 @@
|
|||
currentToastEl = undefined
|
||||
}
|
||||
}, 2500)
|
||||
getStorageItem('disableAutoDismiss').then((disable) => {
|
||||
console.log('got disableAutoDismiss', disable)
|
||||
if (disable) {
|
||||
cancelAutoDismiss()
|
||||
}
|
||||
})
|
||||
statusBox.innerHTML = systemIcons.success
|
||||
break
|
||||
case 'failure':
|
||||
|
|
|
|||
|
|
@ -34,6 +34,21 @@ function clearAPIKey() {
|
|||
})
|
||||
}
|
||||
|
||||
function autoDismissChanged(event) {
|
||||
const value = document.getElementById('disable-auto-dismiss').checked
|
||||
console.log(
|
||||
' value: ',
|
||||
value,
|
||||
document.getElementById('disable-auto-dismiss')
|
||||
)
|
||||
|
||||
setStorage({
|
||||
disableAutoDismiss: value ? 'true' : null,
|
||||
}).then(() => {
|
||||
console.log('disableAutoDismiss updated', value)
|
||||
})
|
||||
}
|
||||
|
||||
;(() => {
|
||||
document
|
||||
.getElementById('save-api-key-btn')
|
||||
|
|
@ -44,4 +59,15 @@ function clearAPIKey() {
|
|||
document
|
||||
.getElementById('clear-api-key-btn')
|
||||
.addEventListener('click', clearAPIKey)
|
||||
|
||||
getStorageItem('disableAutoDismiss').then((value) => {
|
||||
console.log('disableAutoDismiss updated', value)
|
||||
document.getElementById('disable-auto-dismiss').checked = value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
document
|
||||
.getElementById('disable-auto-dismiss')
|
||||
.addEventListener('change', autoDismissChanged)
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@
|
|||
<head>
|
||||
<title>API Key Storage</title>
|
||||
<script src="/scripts/common.js"></script>
|
||||
<style>
|
||||
div.wrapper: {
|
||||
max-width: 480px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
|
|
@ -35,6 +30,14 @@
|
|||
<button id="save-api-key-btn">Save API Key</button>
|
||||
<button id="load-api-key-btn">Load API Key</button>
|
||||
<button id="clear-api-key-btn">Clear API Key</button>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<h1>Settings</h1>
|
||||
|
||||
<input type="checkbox" id="disable-auto-dismiss" />
|
||||
<label for="auto-dismiss">Disable Auto Dismiss</label>
|
||||
|
||||
</div>
|
||||
<script src="/scripts/options.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in a new issue