Skip to content

Commit 60e910e

Browse files
committed
Merge branch 'dev'
2 parents 4125a60 + a81fb7b commit 60e910e

File tree

11 files changed

+117
-29
lines changed

11 files changed

+117
-29
lines changed

apps/backend/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
## [6.1.14] - 2025-08-14
10+
11+
### Fixed
12+
- Push notifications get sent right away after a post is created
13+
- Fixed bugs that were preventing some digest emails and comment digest emails from being sent
14+
915
## [6.1.13] - 2025-08-08
1016

1117
### Changed

apps/backend/api/models/Notification.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,9 @@ module.exports = bookshelf.Model.extend({
967967
.fetchAll(options)
968968
},
969969

970+
// Process a bounded batch of unsent notifications and return.
971+
// If there may be more to process, re-enqueue another job so we don't
972+
// hold a Kue worker slot indefinitely.
970973
sendUnsent: function () {
971974
// FIXME empty out this withRelated list and just load things on demand when
972975
// creating push notifications / emails
@@ -992,16 +995,21 @@ module.exports = bookshelf.Model.extend({
992995
'activity.track'
993996
]
994997
})
995-
.then(ns => ns.length > 0 &&
996-
Promise.each(ns.models,
997-
n => n.send().catch(err => {
998+
.then(async ns => {
999+
if (!ns || ns.length === 0) return
1000+
await Promise.each(ns.models, n =>
1001+
n.send().catch(err => {
9981002
console.error('Error sending notification', err, n.attributes)
9991003
rollbar.error(err, null, { notification: n.attributes })
10001004
return n.save({ failed_at: new Date() }, { patch: true })
1001-
}))
1002-
.then(() => new Promise(resolve => {
1003-
setTimeout(() => resolve(Notification.sendUnsent()), 1000)
1004-
})))
1005+
})
1006+
)
1007+
// If we hit the limit (200), there may be more to process.
1008+
if (ns.length >= 200) {
1009+
// Re-enqueue another pass shortly.
1010+
Queue.classMethod('Notification', 'sendUnsent', {}, 1000)
1011+
}
1012+
})
10051013
},
10061014

10071015
priorityReason: function (reasons) {

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"author": "Hylo <hello@hylo.com>",
66
"license": "Apache-2.0",
77
"private": true,
8-
"version": "6.1.13",
8+
"version": "6.1.14",
99
"nyc": {
1010
"sourceMap": false,
1111
"instrument": false,

apps/backend/worker.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ const throttledLog = throttle(error => {
5959
if (rollbar.disabled) {
6060
sails.log.error(error.message)
6161
} else {
62+
sails.log.error(error.message)
6263
rollbar.error(error)
6364
}
6465
}, 30000)
6566

6667
function handleRedisError (err) {
67-
if (err && err.message && err.message.includes('Redis connection')) {
68+
if (err && err.message) {
6869
throttledLog(err)
6970
}
7071
}

apps/mobile/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
10. `cd ..` (leave the ios folder)
1515
11. `yarn start` (must be within apps/mobile) launches `metro`, the heart of the mobile dev experience
1616
12. You will see the metro options for launching the different environments and the devtools, and reload
17-
13. You will also need to run `yarn run android` to open the correct ports for the android emulator to be able to access the backend. Once it opens the ports, you can just cancel the rest of its actions
17+
13. To build/install the android app run `yarn run android` in a different terminal window.
18+
13. To build/install the ios app run `yarn run ios` in a different terminal window. If it fails with error code 70 you need to specify the destination device or simulator you want to build for. You can select which to build for by running `yarn react-native run-ios --list-devices`
1819

1920
## Quick debug
2021
1. Most code changes will hot-reload into the devices; sometimes you need to hit 'r' in the metro terminal instance to reload (what you see afer hitting `yarn start`)
@@ -123,4 +124,4 @@ Real-time updates are handled in urql by subscriptions. These are integrated int
123124
### Push Notification testing
124125
This requires tweaks to your local backend env; PUSH_NOTIFICATIONS_TESTING_ENABLED needs to be set to TRUE or sometimes you can get away with just adding specific hylo user ids to the HYLO_TESTER_IDS. And you'll need to add valid OneSignal ID and key to your backend env
125126

126-
After you have done this, the quick-n-dirty way to test is to go to the OneSignal model in the backend, insert a url you want to test, and then trigger a notification on a user that you have control of, and that exists in both in your local db and the prod db.
127+
After you have done this, the quick-n-dirty way to test is to go to the OneSignal model in the backend, insert a url you want to test, and then trigger a notification on a user that you have control of, and that exists in both in your local db and the prod db.

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"lint": "standard",
1313
"bump-version": "node scripts/bump-version.js",
1414
"postversion": "react-native-version --never-amend",
15-
"start": "adb reverse tcp:3001 tcp:3001 && adb reverse tcp:3000 tcp:3000 && rm -f node_modules/react-native-config/ios/ReactNativeConfig/GeneratedDotEnv.m node_modules/react-native-config/android/build/generated/source/buildConfig/debug/com/lugg/RNCConfig/BuildConfig.java node_modules/react-native-config/android/build/generated/source/buildConfig/release/com/lugg/RNCConfig/BuildConfig.java && react-native start",
15+
"start": "rm -f node_modules/react-native-config/ios/ReactNativeConfig/GeneratedDotEnv.m node_modules/react-native-config/android/build/generated/source/buildConfig/debug/com/lugg/RNCConfig/BuildConfig.java node_modules/react-native-config/android/build/generated/source/buildConfig/release/com/lugg/RNCConfig/BuildConfig.java && react-native start",
1616
"test": "yarn jest",
1717
"test:linking": "yarn test src/navigation/linking/getStateFromPath.test.js",
1818
"open-link": "scripts/open-link.sh",

apps/mobile/src/screens/GroupWelcome/GroupWelcome.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function GroupWelcome () {
5050
const [{ currentUser }] = useCurrentUser()
5151
const [{ currentGroup }] = useCurrentGroup()
5252
const currentMemberships = currentUser?.memberships
53-
const currentMembership = currentMemberships.find(m => m.group.id === currentGroup?.id)
53+
const currentMembership = currentMemberships?.find(m => m.group.id === currentGroup?.id)
5454
const routeNames = getRouteNames(currentGroup, currentMembership)
5555

5656
const { name, avatarUrl, purpose, bannerUrl, description, agreements, joinQuestions, settings, welcomePage } = currentGroup

apps/mobile/src/screens/Thread/Thread.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export default function Thread () {
137137
inverted
138138
keyExtractor={(item) => item.id}
139139
keyboardDismissMode='on-drag'
140-
keyboardShouldPersistTaps
140+
keyboardShouldPersistTaps='always'
141141
refreshing={fetching}
142142
onEndReached={fetchMore}
143143
onEndReachedThreshold={0.3}

apps/mobile/src/screens/ThreadList/ThreadList.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FlashList } from '@shopify/flash-list'
44
import { useFocusEffect, useNavigation } from '@react-navigation/native'
55
import { useTranslation } from 'react-i18next'
66
import { useMutation, useQuery } from 'urql'
7+
import { v4 as uuidv4 } from 'uuid'
78
import updateUserSettingsMutation from '@hylo/graphql/mutations/updateUserSettingsMutation'
89
import messageThreadsQuery from '@hylo/graphql/queries/messageThreadsQuery'
910
import useCurrentUser from '@hylo/hooks/useCurrentUser'
@@ -12,7 +13,6 @@ import ThreadCard from 'components/ThreadCard'
1213
import styles from './ThreadList.styles'
1314

1415
export default function ThreadList () {
15-
const { t } = useTranslation()
1616
const navigation = useNavigation()
1717
const [{ currentUser }] = useCurrentUser()
1818
const [offset, setOffset] = useState(0)
@@ -60,12 +60,12 @@ export default function ThreadList () {
6060
data={threads}
6161
estimatedItemSize={93}
6262
estimatedListSize={Dimensions.get('screen')}
63-
keyExtractor={item => item.id.toString()}
63+
keyExtractor={item => item?.id?.toString() || uuidv4()}
6464
onEndReached={fetchMoreThreads}
6565
onRefresh={refreshThreads}
6666
refreshing={fetching}
6767
renderItem={({ item, index }) => (
68-
<ThreadCard
68+
item && <ThreadCard
6969
currentUser={currentUser}
7070
isLast={index === threads.length - 1}
7171
message={getLatestMessage(item)}

apps/mobile/src/urql/mobileSubscriptionExchange.js

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { subscriptionExchange } from 'urql'
22
import EventSource from 'react-native-sse'
33
import { URL } from 'react-native-url-polyfill'
4+
import { Platform } from 'react-native'
45
import { GRAPHQL_ENDPOINT_URL } from '@hylo/urql/makeUrqlClient'
56
import { getSessionCookie } from 'util/session'
67

@@ -13,8 +14,11 @@ const subscriptionLoggingOn = process.env.NODE_ENV === 'development' && true //
1314
const exponentialBackoff = (attempt) => Math.min(BASE_RETRY_DELAY_MS * (2 ** attempt), RETRY_CAP_MS)
1415

1516
const connectSubscription = async (url, sink) => {
17+
// Extract query details for logging
18+
const queryText = url.searchParams.get('query')?.substring(0, 50) + '...'
19+
1620
if (subscriptionLoggingOn) {
17-
console.log('Connecting graphql subscription for:', url.searchParams.get('query')?.substring(0, 50) + '...')
21+
console.log(`📱 [${Platform.OS.toUpperCase()}] Connecting graphql subscription for:`, queryText)
1822
}
1923

2024
let retryCount = 0
@@ -30,6 +34,16 @@ const connectSubscription = async (url, sink) => {
3034
// Get session cookie for authentication
3135
const sessionCookie = await getSessionCookie()
3236

37+
if (!sessionCookie) {
38+
console.warn(`📱 [${Platform.OS.toUpperCase()}] No session cookie found, cannot establish subscription connection`)
39+
// Don't retry if there's no session - this will cause authentication issues
40+
handleConnectionError({
41+
message: 'No session cookie - authentication required',
42+
isAuthError: true
43+
})
44+
return
45+
}
46+
3347
const headers = {}
3448
if (sessionCookie) {
3549
headers.Cookie = sessionCookie
@@ -45,7 +59,7 @@ const connectSubscription = async (url, sink) => {
4559

4660
eventSource.addEventListener('open', () => {
4761
if (subscriptionLoggingOn) {
48-
console.log('Subscription connection opened for:', url.searchParams.get('query')?.substring(0, 50) + '...')
62+
console.log(`📱 [${Platform.OS.toUpperCase()}] Subscription connection opened for:`, queryText)
4963
}
5064
resetRetryState()
5165
})
@@ -55,7 +69,7 @@ const connectSubscription = async (url, sink) => {
5569
const data = JSON.parse(event.data)
5670

5771
if (subscriptionLoggingOn) {
58-
console.log('📱 SSE received data:', {
72+
console.log(`📱 [${Platform.OS.toUpperCase()}] SSE received data:`, {
5973
type: data.type || data.event || 'unknown',
6074
keys: Object.keys(data),
6175
errors: data.errors,
@@ -64,19 +78,55 @@ const connectSubscription = async (url, sink) => {
6478
})
6579

6680
if (data?.data?.allUpdates?.__typename === 'Post') {
67-
console.log('📱 SSE received Post update:', data.data.allUpdates.id)
81+
console.log(`📱 [${Platform.OS.toUpperCase()}] SSE received Post update:`, data.data.allUpdates.id)
82+
}
83+
84+
// Handle Heartbeat type to keep connection alive
85+
if (data?.data?.allUpdates?.__typename === 'Heartbeat') {
86+
if (subscriptionLoggingOn) {
87+
console.log(`📱 [${Platform.OS.toUpperCase()}] SSE received Heartbeat:`, data.data.allUpdates.timestamp)
88+
}
89+
// Don't forward heartbeat to sink, just keep connection alive
90+
return
6891
}
6992
}
93+
94+
// Check for the specific "Subscription field must return Async Iterable" error
95+
if (data?.errors && data.errors.some(error =>
96+
error.message && error.message.includes('Subscription field must return Async Iterable')
97+
)) {
98+
console.error(`📱 [${Platform.OS.toUpperCase()}] Critical subscription error detected:`, data.errors)
99+
100+
// Log additional details about the error
101+
if (process.env.NODE_ENV === 'development') {
102+
console.error(`📱 [${Platform.OS.toUpperCase()}] Error details:`, {
103+
errorCount: data.errors.length,
104+
firstError: data.errors[0],
105+
errorMessages: data.errors.map(e => e.message),
106+
errorPaths: data.errors.map(e => e.path),
107+
errorLocations: data.errors.map(e => e.locations)
108+
})
109+
}
110+
111+
// Don't forward this error to the sink as it will cause issues
112+
// Instead, close the connection and let it retry
113+
handleConnectionError({
114+
message: 'Backend subscription configuration error - Async Iterable issue',
115+
isBackendError: true,
116+
errorDetails: data.errors
117+
})
118+
return
119+
}
70120

71121
sink.next(data)
72122
} catch (error) {
73-
console.error('Error parsing SSE message:', error)
123+
console.warn(`📱 [${Platform.OS.toUpperCase()}] Error parsing SSE message:`, error)
74124
}
75125
})
76126

77127
eventSource.addEventListener('close', (event) => {
78128
if (subscriptionLoggingOn) {
79-
console.log('Subscription connection closed.', JSON.stringify(event, null, 2))
129+
console.log(`📱 [${Platform.OS.toUpperCase()}] Subscription connection closed.`, JSON.stringify(event, null, 2))
80130
}
81131
// This event doesn't automatically mean an error, but in our case,
82132
// a close is unexpected, so we'll trigger the reconnection logic.
@@ -86,10 +136,18 @@ const connectSubscription = async (url, sink) => {
86136
const handleConnectionError = (event) => {
87137
if (isReconnecting) return
88138

89-
console.error('Subscription error. Full event:', JSON.stringify(event, null, 2))
90-
console.error('Subscription error message:', event?.message || 'Unknown error')
139+
// console.warn(`📱 [${Platform.OS.toUpperCase()}] Subscription error. Full event:`, JSON.stringify(event, null, 2))
140+
console.warn(`📱 [${Platform.OS.toUpperCase()}] Query text:`, queryText)
141+
console.warn(`📱 [${Platform.OS.toUpperCase()}] Subscription error message:`, event?.message || 'Unknown error')
142+
143+
// For backend configuration errors, use a longer delay to avoid overwhelming the server
144+
if (event?.isBackendError) {
145+
console.warn(`📱 [${Platform.OS.toUpperCase()}] Backend subscription error detected, using extended retry delay`)
146+
retryCount += 2 // Increment retry count more aggressively for backend errors
147+
} else {
148+
retryCount++
149+
}
91150

92-
retryCount++
93151
isReconnecting = true
94152

95153
if (eventSource) {
@@ -98,13 +156,13 @@ const connectSubscription = async (url, sink) => {
98156

99157
// Stop retrying if MAX_RETRIES is set and exceeded (unless it's 0 for infinite retries)
100158
if (MAX_RETRIES > 0 && retryCount >= MAX_RETRIES) {
101-
console.error('Max retries reached. No further attempts.')
159+
console.warn(`📱 [${Platform.OS.toUpperCase()}] Max retries reached. No further attempts.`)
102160
return
103161
}
104162

105163
const retryDelay = exponentialBackoff(retryCount)
106164
if (subscriptionLoggingOn) {
107-
console.log(`Retrying subscription in ${retryDelay / 1000} seconds...`)
165+
console.log(`📱 [${Platform.OS.toUpperCase()}] Retrying subscription for: ${queryText} in ${retryDelay / 1000} seconds...`)
108166
}
109167

110168
setTimeout(() => {
@@ -122,7 +180,7 @@ const connectSubscription = async (url, sink) => {
122180
unsubscribe: () => {
123181
if (eventSource) {
124182
if (subscriptionLoggingOn) {
125-
console.log('Unsubscribing from SSE stream')
183+
console.log(`📱 [${Platform.OS.toUpperCase()}] Unsubscribing from SSE stream for:`, queryText)
126184
}
127185
eventSource.close()
128186
}

0 commit comments

Comments
 (0)