diff --git a/NOTICE.txt b/NOTICE.txt index 336ae5e2a0..8453b77d56 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -200,6 +200,41 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- + +## @mattermost/react-native-turbo-mailer + +This product contains '@mattermost/react-native-turbo-mailer' by Avinash Lingaloo. + +An adaptation of react-native-mail that supports Turbo Module + +* HOMEPAGE: + * https://github.com/mattermost/react-native-turbo-mailer#readme + +* LICENSE: MIT + +MIT License + +Copyright (c) 2022 Mattermost +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --- ## @msgpack/msgpack @@ -493,6 +528,21 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--- + +## @react-navigation/stack + +This product contains '@react-navigation/stack'. + +Stack navigator component for iOS and Android with animated transitions and gestures + +* HOMEPAGE: + * https://reactnavigation.org/docs/stack-navigator/ + +* LICENSE: MIT + + + --- ## @rudderstack/rudder-sdk-react-native @@ -1109,6 +1159,40 @@ Lightweight fuzzy-search limitations under the License. +--- + +## html-entities + +This product contains 'html-entities' by Marat Dulin. + +Fastest HTML entities encode/decode library. + +* HOMEPAGE: + * https://github.com/mdevils/html-entities#readme + +* LICENSE: MIT + +Copyright (c) 2021 Dulin Marat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + --- ## jail-monkey diff --git a/app/actions/remote/command.ts b/app/actions/remote/command.ts index 6ecd317fe3..bcc7c25c19 100644 --- a/app/actions/remote/command.ts +++ b/app/actions/remote/command.ts @@ -138,7 +138,7 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc const match = matchDeepLink(location, serverUrl, config?.SiteURL); if (match) { - handleDeepLink(location, intl, location); + handleDeepLink(match, intl, location); } else { const {formatMessage} = intl; const onError = () => Alert.alert( diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 5f094dbd37..f2b12615b3 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -86,7 +86,7 @@ export async function handleFirstConnect(serverUrl: string) { // ESR: 5.37 if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) { - handleReconnect(serverUrl); + await handleReconnect(serverUrl); return; } @@ -100,8 +100,8 @@ export async function handleFirstConnect(serverUrl: string) { } } -export function handleReconnect(serverUrl: string) { - doReconnect(serverUrl); +export async function handleReconnect(serverUrl: string) { + await doReconnect(serverUrl); } export async function handleClose(serverUrl: string, lastDisconnect: number) { diff --git a/app/components/connection_banner/connection_banner.tsx b/app/components/connection_banner/connection_banner.tsx index ea1340a720..44604ff32d 100644 --- a/app/components/connection_banner/connection_banner.tsx +++ b/app/components/connection_banner/connection_banner.tsx @@ -20,7 +20,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { - isConnected: boolean; + websocketState: WebsocketConnectedState; } const getStyle = makeStyleSheetFromTheme((theme: Theme) => { @@ -74,7 +74,7 @@ const TIME_TO_OPEN = toMilliseconds({seconds: 3}); const TIME_TO_CLOSE = toMilliseconds({seconds: 1}); const ConnectionBanner = ({ - isConnected, + websocketState, }: Props) => { const intl = useIntl(); const closeTimeout = useRef(); @@ -86,6 +86,8 @@ const ConnectionBanner = ({ const appState = useAppState(); const netInfo = useNetInfo(); + const isConnected = websocketState === 'connected'; + const openCallback = useCallback(() => { setVisible(true); clearTimeoutRef(openTimeout); @@ -97,7 +99,9 @@ const ConnectionBanner = ({ }, []); useEffect(() => { - if (!isConnected) { + if (websocketState === 'connecting') { + openCallback(); + } else if (!isConnected) { openTimeout.current = setTimeout(openCallback, TIME_TO_OPEN); } return () => { @@ -158,6 +162,8 @@ const ConnectionBanner = ({ let text; if (isConnected) { text = intl.formatMessage({id: 'connection_banner.connected', defaultMessage: 'Connection restored'}); + } else if (websocketState === 'connecting') { + text = intl.formatMessage({id: 'connection_banner.connecting', defaultMessage: 'Connecting...'}); } else if (netInfo.isInternetReachable) { text = intl.formatMessage({id: 'connection_banner.not_reachable', defaultMessage: 'The server is not reachable'}); } else { diff --git a/app/components/connection_banner/index.ts b/app/components/connection_banner/index.ts index 0b5547837b..5de70a4285 100644 --- a/app/components/connection_banner/index.ts +++ b/app/components/connection_banner/index.ts @@ -9,7 +9,7 @@ import websocket_manager from '@managers/websocket_manager'; import ConnectionBanner from './connection_banner'; const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({ - isConnected: websocket_manager.observeConnected(serverUrl), + websocketState: websocket_manager.observeWebsocketState(serverUrl), })); export default withServerUrl(enhanced(ConnectionBanner)); diff --git a/app/components/markdown/hashtag/index.tsx b/app/components/markdown/hashtag/index.tsx index 2090e80737..17b041c54c 100644 --- a/app/components/markdown/hashtag/index.tsx +++ b/app/components/markdown/hashtag/index.tsx @@ -2,8 +2,9 @@ // See LICENSE.txt for license information. import React from 'react'; -import {StyleProp, Text, TextStyle} from 'react-native'; +import {DeviceEventEmitter, StyleProp, Text, TextStyle} from 'react-native'; +import {Navigation, Screens} from '@constants'; import {popToRoot, dismissAllModals} from '@screens/navigation'; type HashtagProps = { @@ -17,7 +18,12 @@ const Hashtag = ({hashtag, linkStyle}: HashtagProps) => { await dismissAllModals(); await popToRoot(); - // showSearchModal('#' + hashtag); + DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, { + screen: Screens.SEARCH, + params: { + searchTerm: hashtag, + }, + }); }; return ( diff --git a/app/components/markdown/markdown_image/index.tsx b/app/components/markdown/markdown_image/index.tsx index 3f734ee2e6..b266162590 100644 --- a/app/components/markdown/markdown_image/index.tsx +++ b/app/components/markdown/markdown_image/index.tsx @@ -59,7 +59,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ height: 24, }, container: { - marginBottom: 5, + marginVertical: 5, + top: 5, }, svg: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), diff --git a/app/components/markdown/markdown_link/markdown_link.tsx b/app/components/markdown/markdown_link/markdown_link.tsx index 9bab60b803..82fa6d93a7 100644 --- a/app/components/markdown/markdown_link/markdown_link.tsx +++ b/app/components/markdown/markdown_link/markdown_link.tsx @@ -76,9 +76,9 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU const match = matchDeepLink(url, serverUrl, siteURL); if (match) { - const {error} = await handleDeepLink(url, intl); + const {error} = await handleDeepLink(match, intl); if (error) { - tryOpenURL(url, onError); + tryOpenURL(match, onError); } } else { tryOpenURL(url, onError); diff --git a/app/components/post_draft/draft_input/index.tsx b/app/components/post_draft/draft_input/index.tsx index cfb4a276a4..7ded6863bd 100644 --- a/app/components/post_draft/draft_input/index.tsx +++ b/app/components/post_draft/draft_input/index.tsx @@ -24,8 +24,8 @@ type Props = { canShowPostPriority?: boolean; // Post Props - postProps: Post['props']; - updatePostProps: (postProps: Post['props']) => void; + postPriority: PostPriorityData; + updatePostPriority: (postPriority: PostPriorityData) => void; // Cursor Position Handler updateCursorPosition: React.Dispatch>; @@ -110,8 +110,8 @@ export default function DraftInput({ updateCursorPosition, cursorPosition, updatePostInputTop, - postProps, - updatePostProps, + postPriority, + updatePostPriority, setIsFocused, }: Props) { const theme = useTheme(); @@ -155,9 +155,9 @@ export default function DraftInput({ overScrollMode={'never'} disableScrollViewPanResponder={true} > - {Boolean(postProps.priority) && ( + {Boolean(postPriority?.priority) && ( - + )} diff --git a/app/components/post_draft/quick_actions/post_priority_action/index.tsx b/app/components/post_draft/quick_actions/post_priority_action/index.tsx index cc6452f239..0e0ddf2958 100644 --- a/app/components/post_draft/quick_actions/post_priority_action/index.tsx +++ b/app/components/post_draft/quick_actions/post_priority_action/index.tsx @@ -6,7 +6,7 @@ import {useIntl} from 'react-intl'; import {StyleSheet} from 'react-native'; import CompassIcon from '@components/compass_icon'; -import PostPriorityPicker, {PostPriorityData} from '@components/post_priority/post_priority_picker'; +import PostPriorityPicker from '@components/post_priority/post_priority_picker'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {ICON_SIZE} from '@constants/post_draft'; import {useTheme} from '@context/theme'; @@ -15,8 +15,8 @@ import {changeOpacity} from '@utils/theme'; type Props = { testID?: string; - postProps: Post['props']; - updatePostProps: (postProps: Post['props']) => void; + postPriority: PostPriorityData; + updatePostPriority: (postPriority: PostPriorityData) => void; } const style = StyleSheet.create({ @@ -29,30 +29,27 @@ const style = StyleSheet.create({ export default function PostPriorityAction({ testID, - postProps, - updatePostProps, + postPriority, + updatePostPriority, }: Props) { const intl = useIntl(); const theme = useTheme(); const handlePostPriorityPicker = useCallback((postPriorityData: PostPriorityData) => { - updatePostProps((oldPostProps: Post['props']) => ({ - ...oldPostProps, - ...postPriorityData, - })); + updatePostPriority(postPriorityData); dismissBottomSheet(); - }, [updatePostProps]); + }, [updatePostPriority]); const renderContent = useCallback(() => { return ( ); - }, [handlePostPriorityPicker, postProps]); + }, [handlePostPriorityPicker, postPriority]); const onPress = useCallback(() => { bottomSheet({ diff --git a/app/components/post_draft/quick_actions/quick_actions.tsx b/app/components/post_draft/quick_actions/quick_actions.tsx index 10d4592321..944a166d0a 100644 --- a/app/components/post_draft/quick_actions/quick_actions.tsx +++ b/app/components/post_draft/quick_actions/quick_actions.tsx @@ -22,8 +22,8 @@ type Props = { value: string; updateValue: (value: string) => void; addFiles: (file: FileInfo[]) => void; - postProps: Post['props']; - updatePostProps: (postProps: Post['props']) => void; + postPriority: PostPriorityData; + updatePostPriority: (postPriority: PostPriorityData) => void; focus: () => void; } @@ -45,8 +45,8 @@ export default function QuickActions({ maxFileCount, updateValue, addFiles, - postProps, - updatePostProps, + postPriority, + updatePostPriority, focus, }: Props) { const atDisabled = value[value.length - 1] === '@'; @@ -101,8 +101,8 @@ export default function QuickActions({ {isPostPriorityEnabled && canShowPostPriority && ( )} diff --git a/app/components/post_draft/send_handler/send_handler.tsx b/app/components/post_draft/send_handler/send_handler.tsx index 6ffb5fb51d..97c7e7e696 100644 --- a/app/components/post_draft/send_handler/send_handler.tsx +++ b/app/components/post_draft/send_handler/send_handler.tsx @@ -13,6 +13,7 @@ import {setStatus} from '@actions/remote/user'; import {canEndCall, endCall, getEndCallMessage} from '@calls/actions/calls'; import ClientError from '@client/rest/error'; import {Events, Screens} from '@constants'; +import {PostPriorityType} from '@constants/post'; import {NOTIFY_ALL_MEMBERS} from '@constants/post_draft'; import {useServerUrl} from '@context/server'; import DraftUploadManager from '@managers/draft_upload_manager'; @@ -54,6 +55,10 @@ type Props = { uploadFileError: React.ReactNode; } +const INITIAL_PRIORITY = { + priority: PostPriorityType.STANDARD, +}; + export default function SendHandler({ testID, channelId, @@ -83,8 +88,7 @@ export default function SendHandler({ const [channelTimezoneCount, setChannelTimezoneCount] = useState(0); const [sendingMessage, setSendingMessage] = useState(false); - - const [postProps, setPostProps] = useState({}); + const [postPriority, setPostPriority] = useState(INITIAL_PRIORITY); const canSend = useCallback(() => { if (sendingMessage) { @@ -120,17 +124,19 @@ export default function SendHandler({ message: value, } as Post; - if (Object.keys(postProps).length) { - post.props = postProps; + if (Object.keys(postPriority).length) { + post.metadata = { + priority: postPriority, + }; } createPost(serverUrl, post, postFiles); clearDraft(); setSendingMessage(false); - setPostProps({}); + setPostPriority(INITIAL_PRIORITY); DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL); - }, [files, currentUserId, channelId, rootId, value, clearDraft, postProps]); + }, [files, currentUserId, channelId, rootId, value, clearDraft, postPriority]); const showSendToAllOrChannelOrHereAlert = useCallback((calculatedMembersCount: number, atHere: boolean) => { const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(intl, calculatedMembersCount, Boolean(isTimezoneEnabled), channelTimezoneCount, atHere); @@ -297,8 +303,8 @@ export default function SendHandler({ canSend={canSend()} maxMessageLength={maxMessageLength} updatePostInputTop={updatePostInputTop} - postProps={postProps} - updatePostProps={setPostProps} + postPriority={postPriority} + updatePostPriority={setPostPriority} setIsFocused={setIsFocused} /> ); diff --git a/app/components/post_list/post/body/message/message.tsx b/app/components/post_list/post/body/message/message.tsx index 74df04462f..001bbff239 100644 --- a/app/components/post_list/post/body/message/message.tsx +++ b/app/components/post_list/post/body/message/message.tsx @@ -44,6 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { message: { color: theme.centerChannelColor, ...typography('Body', 200), + lineHeight: undefined, // remove line height, not needed and causes problems with md images }, pendingPost: { opacity: 0.5, diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index fafe6035bc..7b9eaab895 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -132,10 +132,10 @@ const Header = (props: HeaderProps) => { style={style.time} testID='post_header.date_time' /> - {showPostPriority && ( + {showPostPriority && post.metadata?.priority?.priority && ( )} diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx index a1e6f1107a..a177816680 100644 --- a/app/components/post_list/post/post.tsx +++ b/app/components/post_list/post/post.tsx @@ -226,7 +226,7 @@ const Post = ({ // If the post is a priority post: // 1. Show the priority label in channel screen // 2. Show the priority label in thread screen for the root post - const showPostPriority = Boolean(isPostPriorityEnabled && post.props?.priority) && (location !== Screens.THREAD || !post.rootId); + const showPostPriority = Boolean(isPostPriorityEnabled && post.metadata?.priority?.priority) && (location !== Screens.THREAD || !post.rootId); const sameSequence = hasReplies ? (hasReplies && post.rootId) : !post.rootId; if (!showPostPriority && hasSameRoot && isConsecutivePost && sameSequence) { diff --git a/app/components/post_priority/post_priority_label.tsx b/app/components/post_priority/post_priority_label.tsx index 3432b65540..e9aeedd1d5 100644 --- a/app/components/post_priority/post_priority_label.tsx +++ b/app/components/post_priority/post_priority_label.tsx @@ -35,7 +35,7 @@ const style = StyleSheet.create({ }); type Props = { - label: PostPriorityType; + label: PostPriorityData['priority']; }; const PostPriorityLabel = ({label}: Props) => { @@ -48,7 +48,7 @@ const PostPriorityLabel = ({label}: Props) => { containerStyle.push(style.urgent); iconName = 'alert-outline'; labelText = intl.formatMessage({id: 'post_priority.label.urgent', defaultMessage: 'URGENT'}); - } else { + } else if (label === PostPriorityType.IMPORTANT) { containerStyle.push(style.important); iconName = 'alert-circle-outline'; labelText = intl.formatMessage({id: 'post_priority.label.important', defaultMessage: 'IMPORTANT'}); diff --git a/app/components/post_priority/post_priority_picker/index.tsx b/app/components/post_priority/post_priority_picker/index.tsx index 9de0b58d0c..48c51e60a1 100644 --- a/app/components/post_priority/post_priority_picker/index.tsx +++ b/app/components/post_priority/post_priority_picker/index.tsx @@ -14,10 +14,6 @@ import {typography} from '@utils/typography'; import PostPriorityPickerItem from './post_priority_picker_item'; -export type PostPriorityData = { - priority: PostPriorityType; -}; - type Props = { data: PostPriorityData; onSubmit: (data: PostPriorityData) => void; @@ -61,8 +57,8 @@ const PostPriorityPicker = ({data, onSubmit}: Props) => { // For now, we just have one option but the spec suggest we have more in the next phase // const [data, setData] = React.useState(defaultData); - const handleUpdatePriority = React.useCallback((priority: PostPriorityType) => { - onSubmit({priority}); + const handleUpdatePriority = React.useCallback((priority: PostPriorityData['priority']) => { + onSubmit({priority: priority || ''}); }, [onSubmit]); return ( diff --git a/app/components/syntax_highlight/index.tsx b/app/components/syntax_highlight/index.tsx index a83b2e3e01..8ddcdd63b0 100644 --- a/app/components/syntax_highlight/index.tsx +++ b/app/components/syntax_highlight/index.tsx @@ -4,7 +4,7 @@ import React, {useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import SyntaxHighlighter from 'react-syntax-highlighter'; -import {github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs'; +import {githubGist as github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs'; import {useTheme} from '@context/theme'; diff --git a/app/database/models/server/category.ts b/app/database/models/server/category.ts index 13296ee142..dfaeeb245a 100644 --- a/app/database/models/server/category.ts +++ b/app/database/models/server/category.ts @@ -104,11 +104,18 @@ export default class CategoryModel extends Model implements CategoryInterface { Q.sortBy('last_post_at', Q.desc), ); - /** hasChannels : Returns a boolean indicating if the category has channels */ - @lazy hasChannels = this.categoryChannels.observeCount().pipe( - map((c) => c > 0), - distinctUntilChanged(), - ); + observeHasChannels = (canViewArchived: boolean) => { + return this.channels.observeWithColumns(['delete_at']).pipe( + map((channels) => { + if (canViewArchived) { + return channels.length > 0; + } + + return channels.filter((c) => c.deleteAt === 0).length > 0; + }), + distinctUntilChanged(), + ); + }; toCategoryWithChannels = async (): Promise => { const categoryChannels = await this.categoryChannels.fetch(); diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index ec78be8f7e..14aac23155 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {AppState, DeviceEventEmitter, Platform} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import { Notification, NotificationAction, @@ -29,6 +28,7 @@ import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread'; import {dismissOverlay, showOverlay} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; +import {isBetaApp} from '@utils/general'; import {isMainActivity, isTablet} from '@utils/helpers'; import {logInfo} from '@utils/log'; import {convertToNotificationData} from '@utils/notification'; @@ -248,7 +248,7 @@ class PushNotifications { if (Platform.OS === 'ios') { prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE; - if (DeviceInfo.getBundleId().includes('rnbeta')) { + if (isBetaApp) { prefix = `${prefix}beta`; } } else { diff --git a/app/managers/websocket_manager.ts b/app/managers/websocket_manager.ts index 809ad34444..1e44a3da2c 100644 --- a/app/managers/websocket_manager.ts +++ b/app/managers/websocket_manager.ts @@ -21,10 +21,10 @@ import {isMainActivity} from '@utils/helpers'; import {logError} from '@utils/log'; const WAIT_TO_CLOSE = toMilliseconds({seconds: 15}); -const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 20}); +const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 5}); class WebsocketManager { - private connectedSubjects: {[serverUrl: string]: BehaviorSubject} = {}; + private connectedSubjects: {[serverUrl: string]: BehaviorSubject} = {}; private clients: Record = {}; private connectionTimerIDs: Record void>> = {}; @@ -69,7 +69,7 @@ class WebsocketManager { } delete this.clients[serverUrl]; - this.getConnectedSubject(serverUrl).next(false); + this.getConnectedSubject(serverUrl).next('not_connected'); delete this.connectedSubjects[serverUrl]; }; @@ -96,7 +96,7 @@ class WebsocketManager { const client = this.clients[url]; if (client.isConnected()) { client.close(true); - this.getConnectedSubject(url).next(false); + this.getConnectedSubject(url).next('not_connected'); } } }; @@ -107,6 +107,7 @@ class WebsocketManager { if (clientUrl === activeServerUrl) { this.initializeClient(clientUrl); } else { + this.getConnectedSubject(clientUrl).next('connecting'); const bounce = debounce(this.initializeClient.bind(this, clientUrl), WAIT_UNTIL_NEXT); this.connectionTimerIDs[clientUrl] = bounce; bounce(); @@ -118,7 +119,7 @@ class WebsocketManager { return this.clients[serverUrl]?.isConnected(); }; - public observeConnected = (serverUrl: string) => { + public observeWebsocketState = (serverUrl: string) => { return this.getConnectedSubject(serverUrl).asObservable().pipe( distinctUntilChanged(), ); @@ -126,7 +127,7 @@ class WebsocketManager { private getConnectedSubject = (serverUrl: string) => { if (!this.connectedSubjects[serverUrl]) { - this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl)); + this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl) ? 'connected' : 'not_connected'); } return this.connectedSubjects[serverUrl]; @@ -153,13 +154,13 @@ class WebsocketManager { private onFirstConnect = (serverUrl: string) => { this.startPeriodicStatusUpdates(serverUrl); handleFirstConnect(serverUrl); - this.getConnectedSubject(serverUrl).next(true); + this.getConnectedSubject(serverUrl).next('connected'); }; - private onReconnect = (serverUrl: string) => { + private onReconnect = async (serverUrl: string) => { this.startPeriodicStatusUpdates(serverUrl); - handleReconnect(serverUrl); - this.getConnectedSubject(serverUrl).next(true); + await handleReconnect(serverUrl); + this.getConnectedSubject(serverUrl).next('connected'); }; private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => { @@ -168,7 +169,7 @@ class WebsocketManager { await handleClose(serverUrl, lastDisconnect); this.stopPeriodicStatusUpdates(serverUrl); - this.getConnectedSubject(serverUrl).next(false); + this.getConnectedSubject(serverUrl).next('not_connected'); } }; diff --git a/app/queries/servers/post.ts b/app/queries/servers/post.ts index aae4ed41e2..20de391292 100644 --- a/app/queries/servers/post.ts +++ b/app/queries/servers/post.ts @@ -209,6 +209,7 @@ export const queryPinnedPostsInChannel = (database: Database, channelId: string) Q.where('channel_id', channelId), Q.where('is_pinned', Q.eq(true)), ), + Q.sortBy('create_at', Q.asc), ); }; diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 2780a9e276..5306d7e1f2 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -404,7 +404,13 @@ export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, t export const observeLastUnreadChannelId = (database: Database): Observable => { return querySystemValue(database, SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID).observe().pipe( switchMap((result) => (result.length ? result[0].observe() : of$({value: ''}))), - switchMap((model) => of$(model.value)), + switchMap((model) => { + if (model.value) { + return of$(model.value); + } + + return observeCurrentChannelId(database); + }), ); }; diff --git a/app/queries/servers/thread.ts b/app/queries/servers/thread.ts index 4ee78d7dbd..4a200ad8ef 100644 --- a/app/queries/servers/thread.ts +++ b/app/queries/servers/thread.ts @@ -141,7 +141,10 @@ export const prepareThreadsFromReceivedPosts = async (operator: ServerDataOperat }; export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnreads?: boolean, hasReplies?: boolean, isFollowing?: boolean, sort?: boolean, earliest?: number): Query => { - const query: Q.Clause[] = []; + const query: Q.Clause[] = [ + Q.experimentalNestedJoin(POST, CHANNEL), + Q.on(POST, Q.on(CHANNEL, Q.where('delete_at', 0))), + ]; if (isFollowing) { query.push(Q.where('is_following', true)); @@ -189,30 +192,34 @@ export const queryThreads = (database: Database, teamId?: string, onlyUnreads = Q.where('reply_count', Q.gt(0)), ]; + // Only get threads from available channel + const channelCondition: Q.Condition[] = [ + Q.where('delete_at', 0), + ]; + // If teamId is specified, only get threads in that team if (teamId) { - let condition: Q.Condition = Q.where('team_id', teamId); - if (includeDmGm) { - condition = Q.or( - Q.where('team_id', teamId), - Q.where('team_id', ''), + channelCondition.push( + Q.or( + Q.where('team_id', teamId), + Q.where('team_id', ''), + ), ); + } else { + channelCondition.push(Q.where('team_id', teamId)); } - - query.push( - Q.experimentalNestedJoin(POST, CHANNEL), - Q.on(POST, Q.on(CHANNEL, condition)), - ); } else if (!includeDmGm) { // fetching all threads from all teams // excluding DM/GM channels - query.push( - Q.experimentalNestedJoin(POST, CHANNEL), - Q.on(POST, Q.on(CHANNEL, Q.where('team_id', Q.notEq('')))), - ); + channelCondition.push(Q.where('team_id', Q.notEq(''))); } + query.push( + Q.experimentalNestedJoin(POST, CHANNEL), + Q.on(POST, Q.on(CHANNEL, Q.and(...channelCondition))), + ); + if (onlyUnreads) { query.push(Q.where('unread_replies', Q.gt(0))); } diff --git a/app/screens/home/channel_list/categories_list/categories/body/index.ts b/app/screens/home/channel_list/categories_list/categories/body/index.ts index 4ec32b9a56..9182d24bb9 100644 --- a/app/screens/home/channel_list/categories_list/categories/body/index.ts +++ b/app/screens/home/channel_list/categories_list/categories/body/index.ts @@ -162,8 +162,6 @@ const enhance = withObservables(['category', 'isTablet', 'locale'], ({category, return { limit, sortedChannels, - notifyProps, - lastUnreadId, unreadsOnTop, unreadIds, category, diff --git a/app/screens/home/channel_list/categories_list/categories/header/index.ts b/app/screens/home/channel_list/categories_list/categories/header/index.ts index c543400d97..27048f40f5 100644 --- a/app/screens/home/channel_list/categories_list/categories/header/index.ts +++ b/app/screens/home/channel_list/categories_list/categories/header/index.ts @@ -2,14 +2,23 @@ // See LICENSE.txt for license information. import withObservables from '@nozbe/with-observables'; +import {switchMap} from 'rxjs/operators'; + +import {observeConfigBooleanValue} from '@queries/servers/system'; import CategoryHeader from './header'; import type CategoryModel from '@typings/database/models/servers/category'; -const enhanced = withObservables(['category'], ({category}: {category: CategoryModel}) => ({ - category, - hasChannels: category.hasChannels, -})); +const enhanced = withObservables(['category'], ({category}: {category: CategoryModel}) => { + const canViewArchived = observeConfigBooleanValue(category.database, 'ExperimentalViewArchivedChannels'); + + return { + category, + hasChannels: canViewArchived.pipe( + switchMap((canView) => category.observeHasChannels(canView)), + ), + }; +}); export default enhanced(CategoryHeader); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts index de3f977534..b3379ffe77 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts +++ b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts @@ -8,7 +8,7 @@ import WebsocketManager from '@managers/websocket_manager'; import WebSocket from './websocket'; const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({ - isConnected: WebsocketManager.observeConnected(serverUrl), + websocketState: WebsocketManager.observeWebsocketState(serverUrl), })); export default enhanced(WebSocket); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx index 19cac50798..f4c162a23e 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx @@ -11,7 +11,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { - isConnected: boolean; + websocketState: WebsocketConnectedState; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ @@ -28,10 +28,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ }, })); -const WebSocket = ({isConnected}: Props) => { +const WebSocket = ({websocketState}: Props) => { const theme = useTheme(); - if (isConnected) { + if (websocketState === 'connected' || websocketState === 'connecting') { return null; } diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index f10dd256b5..c9aadd2310 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -109,6 +109,14 @@ const SearchScreen = ({teamId, teams}: Props) => { setSearchTeamId(teamId); }, [teamId]); + useEffect(() => { + if (searchTerm) { + resetToInitial(); + setSearchValue(searchTerm); + handleSearch(searchTeamId, searchTerm); + } + }, [searchTerm]); + const onSnap = (offset: number, animated = true) => { scrollRef.current?.scrollToOffset({offset, animated}); }; diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index e8b1882097..8c77176e1b 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -5,7 +5,7 @@ import merge from 'deepmerge'; import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native'; -import {ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton, ScreenPoppedEvent} from 'react-native-navigation'; +import {ComponentWillAppearEvent, ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton, ScreenPoppedEvent} from 'react-native-navigation'; import tinyColor from 'tinycolor2'; import CompassIcon from '@components/compass_icon'; @@ -32,6 +32,7 @@ const alpha = { export function registerNavigationListeners() { Navigation.events().registerScreenPoppedListener(onPoppedListener); Navigation.events().registerCommandListener(onCommandListener); + Navigation.events().registerComponentWillAppearListener(onScreenWillAppear); } function onCommandListener(name: string, params: any) { @@ -66,7 +67,10 @@ function onCommandListener(name: string, params: any) { function onPoppedListener({componentId}: ScreenPoppedEvent) { // screen pop does not trigger registerCommandListener, but does trigger screenPoppedListener NavigationStore.removeScreenFromStack(componentId); - if (NavigationStore.getVisibleScreen() === Screens.HOME) { +} + +function onScreenWillAppear(event: ComponentWillAppearEvent) { + if (event.componentId === Screens.HOME) { DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true); } } diff --git a/app/screens/sso/sso_with_redirect_url.tsx b/app/screens/sso/sso_with_redirect_url.tsx index 30867c24de..5eb39f7a62 100644 --- a/app/screens/sso/sso_with_redirect_url.tsx +++ b/app/screens/sso/sso_with_redirect_url.tsx @@ -6,13 +6,13 @@ import React, {useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; import {Linking, Platform, Text, View} from 'react-native'; import Button from 'react-native-button'; -import DeviceInfo from 'react-native-device-info'; import urlParse from 'url-parse'; import FormattedText from '@components/formatted_text'; import {Sso} from '@constants'; import NetworkManager from '@managers/network_manager'; import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; +import {isBetaApp} from '@utils/general'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {tryOpenURL} from '@utils/url'; @@ -62,7 +62,7 @@ const SSOWithRedirectURL = ({doSSOLogin, loginError, loginUrl, serverUrl, setLog const style = getStyleSheet(theme); const intl = useIntl(); let customUrlScheme = Sso.REDIRECT_URL_SCHEME; - if (DeviceInfo.getBundleId && DeviceInfo.getBundleId().includes('rnbeta')) { + if (isBetaApp) { customUrlScheme = Sso.REDIRECT_URL_SCHEME_DEV; } diff --git a/app/utils/error_handling.ts b/app/utils/error_handling.ts index 721a05b68b..2ac1145fda 100644 --- a/app/utils/error_handling.ts +++ b/app/utils/error_handling.ts @@ -11,6 +11,7 @@ import { import {DEFAULT_LOCALE, getTranslations, t} from '@i18n'; import {dismissAllModals} from '@screens/navigation'; import {ClientError} from '@utils/client_error'; +import {isBetaApp} from '@utils/general'; import { captureException, captureJSException, @@ -42,7 +43,10 @@ class JavascriptAndNativeErrorHandler { } logWarning('Handling Javascript error', e, isFatal); - captureJSException(e, isFatal); + + if (isBetaApp || isFatal) { + captureJSException(e, isFatal); + } if (isFatal && e instanceof Error) { const translations = getTranslations(DEFAULT_LOCALE); diff --git a/app/utils/general/index.ts b/app/utils/general/index.ts index 60bc8d2793..1a29fa1ba1 100644 --- a/app/utils/general/index.ts +++ b/app/utils/general/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import DeviceInfo from 'react-native-device-info'; import ReactNativeHapticFeedback, {HapticFeedbackTypes} from 'react-native-haptic-feedback'; type SortByCreatAt = (Session | Channel | Team | Post) & { @@ -51,3 +52,5 @@ export const sortByNewest = (a: SortByCreatAt, b: SortByCreatAt) => { return 1; }; + +export const isBetaApp = DeviceInfo.getBundleId && DeviceInfo.getBundleId().includes('rnbeta'); diff --git a/app/utils/sentry.ts b/app/utils/sentry.ts index cf071c10b3..44b5e7bb99 100644 --- a/app/utils/sentry.ts +++ b/app/utils/sentry.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {Database} from '@nozbe/watermelondb'; -import {Breadcrumb} from '@sentry/types'; +import {Breadcrumb, Event} from '@sentry/types'; import {Platform} from 'react-native'; import {Navigation} from 'react-native-navigation'; @@ -10,6 +10,7 @@ import Config from '@assets/config.json'; import DatabaseManager from '@database/manager'; import {getConfig} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; +import {isBetaApp} from '@utils/general'; import {ClientError} from './client_error'; import {logError, logWarning} from './log'; @@ -39,9 +40,18 @@ export function initializeSentry() { return; } + const mmConfig = { + environment: isBetaApp ? 'beta' : 'production', + tracesSampleRate: isBetaApp ? 1.0 : 0.2, + sampleRate: isBetaApp ? 1.0 : 0.2, + attachStacktrace: isBetaApp, // For Beta, stack traces are automatically attached to all messages logged + }; + Sentry.init({ dsn, - tracesSampleRate: 0.2, + sendDefaultPii: false, + ...mmConfig, + ...Config.SentryOptions, integrations: [ new Sentry.ReactNativeTracing({ @@ -51,8 +61,13 @@ export function initializeSentry() { ), }), ], - sendDefaultPii: false, - ...Config.SentryOptions, + beforeSend: (event: Event) => { + if (isBetaApp || event?.level === 'fatal') { + return event; + } + + return null; + }, }); } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3fad4a6b8d..7e281fb267 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -568,6 +568,9 @@ platform :ios do app_name_sub = app_name.gsub(" ", "_") config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' method = ENV['IOS_BUILD_EXPORT_METHOD'].nil? || ENV['IOS_BUILD_EXPORT_METHOD'].empty? ? 'ad-hoc' : ENV['IOS_BUILD_EXPORT_METHOD'] + + # Need to add xcargs to only notification and + xcargs = ENV['SENTRY_ENABLED'] == 'true' ? "SENTRY_DSN_IOS='#{ENV['SENTRY_DSN_IOS']}' SENTRY_ENABLED='#{ENV['SENTRY_ENABLED']}'" : '' setup_code_signing @@ -583,7 +586,8 @@ platform :ios do export_options: { signingStyle: 'manual', iCloudContainerEnvironment: 'Production' - } + }, + xcargs:xcargs ) end diff --git a/ios/ErrorReporting/Sentry.swift b/ios/ErrorReporting/Sentry.swift new file mode 100644 index 0000000000..71b8def203 --- /dev/null +++ b/ios/ErrorReporting/Sentry.swift @@ -0,0 +1,27 @@ +// +// Sentry.swift +// Mattermost +// +// Created by Avinash Lingaloo on 20/12/2022. +// Copyright © 2022 Facebook. All rights reserved. +// +import Foundation + +import Sentry + +func initSentryAppExt(){ + if let SENTRY_ENABLED = Bundle.main.infoDictionary?["SENTRY_ENABLED"] as? String, + let SENTRY_DSN = Bundle.main.infoDictionary?["SENTRY_DSN_IOS"] as? String { + if(SENTRY_ENABLED=="true"){ + SentrySDK.start { options in + options.dsn = SENTRY_DSN + options.enableAppHangTracking = true + options.enableCaptureFailedRequests = true + } + } + } +} + +func testSentry(msg: String){ + SentrySDK.capture(message: msg) +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift index 6c8ca3b504..1b5cd500c1 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift @@ -35,14 +35,25 @@ extension Database { } public func getChannelMentions(_ db: Connection) -> Int { - let mentionsCol = Expression("mentions_count") - let mentions = try? db.scalar(myChannelTable.select(mentionsCol.total)) + let stmtString = """ + SELECT SUM(my.mentions_count) \ + FROM MyChannel my \ + INNER JOIN Channel c ON c.id=my.id \ + WHERE c.delete_at = 0 + """ + let mentions = try? db.prepare(stmtString).scalar() as? Double return Int(mentions ?? 0) } - + public func getThreadMentions(_ db: Connection) -> Int { - let mentionsCol = Expression("unread_mentions") - let mentions = try? db.scalar(threadTable.select(mentionsCol.total)) + let stmtString = """ + SELECT SUM(unread_mentions) \ + FROM Thread t + INNER JOIN Post p ON t.id=p.id \ + INNER JOIN Channel c ON p.channel_id=c.id + WHERE c.delete_at = 0 + """ + let mentions = try? db.prepare(stmtString).scalar() as? Double return Int(mentions ?? 0) } diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift index 830c7941a3..cd4f46f43a 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift @@ -181,8 +181,9 @@ extension Database { public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false, _ receivingThreads: Bool = false) throws { let sortedChainedPosts = chainAndSortPosts(postData) try insertOrUpdatePosts(db, sortedChainedPosts, channelId) - let earliest = sortedChainedPosts.first!.create_at - let latest = sortedChainedPosts.last!.create_at + let sortedAndNotDeletedPosts = sortedChainedPosts.filter({$0.delete_at == 0}) + let earliest = sortedAndNotDeletedPosts.first!.create_at + let latest = sortedAndNotDeletedPosts.last!.create_at if (!receivingThreads) { try handlePostsInChannel(db, channelId, earliest, latest, usedSince) @@ -564,7 +565,7 @@ extension Database { var postsInThread = [String: [Post]]() for post in posts { - if !post.root_id.isEmpty { + if !post.root_id.isEmpty && post.delete_at == 0 { var threadPosts = postsInThread[post.root_id] ?? [Post]() threadPosts.append(post) diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 02f0ef2c02..11feef5184 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 27C667A329523ECA00E590D5 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 27C667A229523ECA00E590D5 /* Sentry */; }; + 27C667A529523F0A00E590D5 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 27C667A429523F0A00E590D5 /* Sentry */; }; + 27C667A9295241B600E590D5 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C667A8295241B600E590D5 /* Sentry.swift */; }; + 27C667AA295241B600E590D5 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C667A8295241B600E590D5 /* Sentry.swift */; }; 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; }; 536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; }; 58495E36BF1A4EAB93609E57 /* Metropolis-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 54956DEFEBB74EF78C3A6AE5 /* Metropolis-SemiBold.ttf */; }; @@ -156,6 +160,7 @@ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mattermost/main.m; sourceTree = ""; }; 182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.release.xcconfig"; sourceTree = ""; }; 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = ""; }; + 27C667A8295241B600E590D5 /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = ""; }; 297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.release.xcconfig"; sourceTree = ""; }; 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-BoldItalic.ttf"; path = "../assets/fonts/OpenSans-BoldItalic.ttf"; sourceTree = ""; }; 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBold.ttf"; path = "../assets/fonts/OpenSans-ExtraBold.ttf"; sourceTree = ""; }; @@ -279,6 +284,7 @@ buildActionMask = 2147483647; files = ( 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */, + 27C667A329523ECA00E590D5 /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -288,6 +294,7 @@ files = ( 7FD4822C2864D73300A5B18B /* OpenGraph in Frameworks */, 7F4288042865D340006B48E1 /* Gekidou in Frameworks */, + 27C667A529523F0A00E590D5 /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -352,6 +359,14 @@ name = Mattermost; sourceTree = ""; }; + 27C667AB2952425700E590D5 /* ErrorReporting */ = { + isa = PBXGroup; + children = ( + 27C667A8295241B600E590D5 /* Sentry.swift */, + ); + path = ErrorReporting; + sourceTree = ""; + }; 33E107B4DC21A5C48B09F800 /* Pods */ = { isa = PBXGroup; children = ( @@ -625,6 +640,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 27C667AB2952425700E590D5 /* ErrorReporting */, 13B07FAE1A68108700A75B9A /* Mattermost */, 7F581D33221ED5C60099E66B /* NotificationService */, 7F292A701E8AB73400A450A3 /* SplashScreenResource */, @@ -698,6 +714,7 @@ name = NotificationService; packageProductDependencies = ( 49AE370026D4455D00EF4E52 /* Gekidou */, + 27C667A229523ECA00E590D5 /* Sentry */, ); productName = NotificationService; productReference = 7F581D32221ED5C60099E66B /* NotificationService.appex */; @@ -719,6 +736,7 @@ packageProductDependencies = ( 7FD4822B2864D73300A5B18B /* OpenGraph */, 7F4288032865D340006B48E1 /* Gekidou */, + 27C667A429523F0A00E590D5 /* Sentry */, ); productName = MattermostShare; productReference = 7FC5698628563FDB000B0905 /* MattermostShare.appex */; @@ -782,7 +800,8 @@ ); mainGroup = 83CBB9F61A601CBA00E9B192; packageReferences = ( - 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */, + 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */, + 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, ); productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; @@ -1005,6 +1024,7 @@ buildActionMask = 2147483647; files = ( 7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */, + 27C667A9295241B600E590D5 /* Sentry.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1021,6 +1041,7 @@ 7F93AAB8287778090047B89F /* Publishers.swift in Sources */, 7F7E9F732864E8060064BFAF /* CompassIcons.swift in Sources */, 7F93AABA28777A390047B89F /* Notification.swift in Sources */, + 27C667AA295241B600E590D5 /* Sentry.swift in Sources */, 7FA9A9902868BD8800AB35A1 /* LocalFileManager.swift in Sources */, 7F93AA9E2875FD310047B89F /* CancelButton.swift in Sources */, 7F42880A286672F6006B48E1 /* ServerService.swift in Sources */, @@ -1502,7 +1523,15 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */ = { + 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; + requirement = { + branch = 8.0.0; + kind = branch; + }; + }; + 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/satoshi-takano/OpenGraph.git"; requirement = { @@ -1513,6 +1542,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 27C667A229523ECA00E590D5 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; + 27C667A429523F0A00E590D5 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; 49AE370026D4455D00EF4E52 /* Gekidou */ = { isa = XCSwiftPackageProductDependency; productName = Gekidou; @@ -1527,7 +1566,7 @@ }; 7FD4822B2864D73300A5B18B /* OpenGraph */ = { isa = XCSwiftPackageProductDependency; - package = 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */; + package = 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */; productName = OpenGraph; }; /* End XCSwiftPackageProductDependency section */ diff --git a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved index 72a8937d2e..edc1406c88 100644 --- a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version": "1.4.1" } }, + { + "package": "Sentry", + "repositoryURL": "https://github.com/getsentry/sentry-cocoa.git", + "state": { + "branch": "8.0.0", + "revision": "1a18683901844a2970ccfb633e4ebae565361817", + "version": null + } + }, { "package": "SQLite.swift", "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index d738e7e41d..814d39d4c0 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -50,8 +50,8 @@ NSExtensionActivationRule - NSExtensionActivationDictionaryVersion - 2 + NSExtensionActivationDictionaryVersion + 2 NSExtensionActivationSupportsAttachmentsWithMaxCount 10 NSExtensionActivationSupportsFileWithMaxCount @@ -73,5 +73,9 @@ NSExtensionPointIdentifier com.apple.share-services + SENTRY_DSN_IOS + $(SENTRY_DSN_IOS) + SENTRY_ENABLED + $(SENTRY_ENABLED) diff --git a/ios/MattermostShare/ShareViewController.swift b/ios/MattermostShare/ShareViewController.swift index f7e0cc8d58..e70489dd1a 100644 --- a/ios/MattermostShare/ShareViewController.swift +++ b/ios/MattermostShare/ShareViewController.swift @@ -10,6 +10,7 @@ import Gekidou import SwiftUI import UIKit import os.log +import Sentry class ShareViewController: UIViewController { private var fileManager: LocalFileManager? @@ -20,7 +21,6 @@ class ShareViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.isModalInPresentation = true - self.addObservers() fileManager = LocalFileManager() if let inputItems = extensionContext?.inputItems { @@ -34,6 +34,9 @@ class ShareViewController: UIViewController { ) }) } + + // Initialize Sentry + initSentryAppExt() } override func viewDidAppear(_ animated: Bool) { @@ -94,6 +97,8 @@ class ShareViewController: UIViewController { let fileCount = attachments.count let files: [String] = attachments.map{ $0.fileUrl.absoluteString } + + var message = text if linkPreviewUrl != nil && !linkPreviewUrl!.isEmpty { if text.isEmpty { diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 09ec58b325..9e2224e0aa 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -29,5 +29,9 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).NotificationService + SENTRY_DSN_IOS + $(SENTRY_DSN_IOS) + SENTRY_ENABLED + $(SENTRY_ENABLED) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 88d4078316..9f0810ffac 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -9,6 +9,11 @@ class NotificationService: UNNotificationServiceExtension { var retryIndex = 0 + override init() { + super.init() + initSentryAppExt() + } + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler @@ -24,7 +29,6 @@ class NotificationService: UNNotificationServiceExtension { func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent, contentHandler: ((UNNotificationContent) -> Void)?) { bestAttemptContent.userInfo["server_url"] = serverUrl - let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any] if let json = json { if let message = json["message"] as? String { diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts index c36824e4f5..51da8a0bfb 100644 --- a/types/api/posts.d.ts +++ b/types/api/posts.d.ts @@ -20,6 +20,10 @@ type PostType = type PostEmbedType = 'image' | 'message_attachment' | 'opengraph'; +type PostPriorityData = { + priority: ''|'urgent'|'important'; +}; + type PostEmbed = { type: PostEmbedType; url: string; @@ -39,6 +43,7 @@ type PostMetadata = { files?: FileInfo[]; images?: Dictionary; reactions?: Reaction[]; + priority?: PostPriorityData; }; type Post = { diff --git a/types/database/models/servers/category.ts b/types/database/models/servers/category.ts index fef270ae83..2b529fe816 100644 --- a/types/database/models/servers/category.ts +++ b/types/database/models/servers/category.ts @@ -58,7 +58,7 @@ declare class CategoryModel extends Model { @lazy myChannels: Query; /** hasChannels : Whether the category has any channels */ - @lazy hasChannels: Observable; + observeHasChannels(canViewArchived: boolean): Observable; /** toCategoryWithChannels returns a map of the Category with an array of ordered channel ids */ toCategoryWithChannels(): Promise; diff --git a/types/global/websocket.d.ts b/types/global/websocket.d.ts new file mode 100644 index 0000000000..9ee8f65e5e --- /dev/null +++ b/types/global/websocket.d.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type WebsocketConnectedState = 'not_connected' | 'connected' | 'connecting';