Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show notifications from others #2491

Merged
merged 17 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/api/observations.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const fetchRemoteObservations = async (
uuids: Array<string>,
params: Object = {},
opts: Object = {}
): Promise<?number> => {
): Promise<?Array<Object>> => {
try {
const response = await inatjs.observations.fetch(
uuids,
Expand Down Expand Up @@ -157,11 +157,12 @@ const fetchObservationUpdates = async (
};

const fetchUnviewedObservationUpdatesCount = async (
opts: Object
params: Object = {},
opts: Object = {}
): Promise<number> => {
try {
const { total_results: updatesCount } = await inatjs.observations.updates( {
observations_by: "owner",
...params,
viewed: false,
per_page: 0
}, opts );
Expand Down
105 changes: 95 additions & 10 deletions src/api/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
// Generic types, please keep alphabetized
export interface ApiOpts {
api_token?: string;
}

export interface ApiParams {
per_page?: number;
page?: number;
fields?: "all" | Object
fields?: "all" | Object;
ttl?: number;
}

export interface ApiOpts {
api_token?: string;
export interface ApiPlace {
id?: number;
name?: string;
}

export interface ApiProject {
id?: number;
title?: string;
}

export interface ApiResponse {
Expand All @@ -15,22 +27,95 @@ export interface ApiResponse {
results: Object[];
}

export interface ApiPlace {
id?: number;
name?: string;
export interface ApiObservationsUpdatesParams extends ApiParams {
observations_by?: "owner" | "following";
}

export interface ApiProject {
id?: number;
title?: string;
}
// Model types, need to be ordered by reference

export interface ApiTaxon {
id?: number;
name?: string;
preferred_common_name?: string;
}

export interface ApiUser {
id?: number;
login?: string;
}

export interface ApiComment {
body?: string;
user?: ApiUser;
}

export interface ApiIdentification {
body?: string;
taxon?: ApiTaxon;
user?: ApiUser;
}

export interface ApiNotification {
comment?: ApiComment;
comment_id?: number;
created_at: string;
id: number;
identification?: ApiIdentification;
identification_id?: number;
notifier_type: string;
resource_uuid: string;
viewed?: boolean;
}

interface ApiFlag {
id?: number;
}

interface ApiModeratorAction {
id?: number;
}

interface ApiMedia {
attribution?: string;
flags?: ApiFlag[];
hidden?: boolean;
id?: number;
license_code?: string;
moderator_actions?: ApiModeratorAction[];
uuid?: string;
}

interface ApiPhoto extends ApiMedia {
original_dimensions?: {
height?: number;
width?: number;
};
url?: string;
}

interface ApiSound extends ApiMedia {
file_content_type?: string;
file_url?: string;
native_sound_id?: number;
}

export interface ApiObservationPhoto {
id?: number;
photo?: ApiPhoto;
position?: number;
uuid?: string;
}

export interface ApiObservationSound {
id?: number;
sound?: ApiSound;
position?: number;
uuid?: string;
}

export interface ApiObservation {
observation_photos?: ApiObservationPhoto[];
observation_sounds?: ApiObservationSound[];
user?: ApiUser;
uuid: string;
}
2 changes: 2 additions & 0 deletions src/components/Developer/UiLibrary/Typography.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Heading3,
Heading4,
Heading5,
Heading6,
List2,
ScrollViewWrapper,
Subheading1,
Expand All @@ -34,6 +35,7 @@ const Typography = ( ) => {
Heading4 (non-default color)
</Heading4>
<Heading5 className="my-2">Heading5</Heading5>
<Heading6 className="my-2">Heading6</Heading6>
<Subheading1 className="my-2">Subheading1</Subheading1>
<Body1 className="my-2">Body1</Body1>
<Body2 className="my-2">Body2</Body2>
Expand Down
54 changes: 54 additions & 0 deletions src/components/Notifications/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Tabs,
ViewWrapper
} from "components/SharedComponents";
import React, { useState } from "react";
import { EventRegister } from "react-native-event-listeners";
import { useTranslation } from "sharedHooks";

import NotificationsContainer from "./NotificationsContainer";
import NotificationsTab, {
NOTIFICATIONS_REFRESHED,
OTHER_TAB,
OWNER_TAB
} from "./NotificationsTab";

const Notifications = ( ) => {
const [activeTab, setActiveTab] = useState<typeof OWNER_TAB | typeof OTHER_TAB>( OWNER_TAB );
const { t } = useTranslation();

return (
<ViewWrapper>
<Tabs
tabs={[
{
id: OWNER_TAB,
text: t( "MY-OBSERVATIONS--notifications" ),
onPress: () => setActiveTab( OWNER_TAB )
},
{
id: OTHER_TAB,
text: t( "OTHER-OBSERVATIONS--notifications" ),
onPress: () => setActiveTab( OTHER_TAB )
}
]}
activeId={activeTab}
TabComponent={NotificationsTab}
/>
{activeTab === OWNER_TAB && (
<NotificationsContainer
notificationParams={{ observations_by: "owner" }}
onRefresh={( ) => EventRegister.emit( NOTIFICATIONS_REFRESHED, OWNER_TAB )}
/>
)}
{activeTab === OTHER_TAB && (
<NotificationsContainer
notificationParams={{ observations_by: "following" }}
onRefresh={( ) => EventRegister.emit( NOTIFICATIONS_REFRESHED, OTHER_TAB )}
/>
)}
</ViewWrapper>
);
};

export default Notifications;
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
// @flow
import {
useNetInfo
} from "@react-native-community/netinfo";
import { useNavigation } from "@react-navigation/native";
import NotificationsList from "components/Notifications/NotificationsList";
import type { Node } from "react";
import type { ApiObservationsUpdatesParams } from "api/types";
import NotificationsList from "components/Notifications/NotificationsList.tsx";
import React, { useEffect, useState } from "react";
import { log } from "sharedHelpers/logger";
import { useInfiniteNotificationsScroll, usePerformance } from "sharedHooks";
import { isDebugMode } from "sharedHooks/useDebugMode";

const logger = log.extend( "NotificationsContainer" );

const NotificationsContainer = (): Node => {
interface Props {
notificationParams: ApiObservationsUpdatesParams;
onRefresh?: ( ) => void;
}

const NotificationsContainer = ( {
notificationParams,
onRefresh: onRefreshProp
}: Props ) => {
const navigation = useNavigation( );
const { isConnected } = useNetInfo( );
const [refreshing, setRefreshing] = useState( false );

const {
notifications,
fetchNextPage,
refetch,
isInitialLoading,
isError,
isFetching,
isError
} = useInfiniteNotificationsScroll( );
isInitialLoading,
notifications,
refetch
} = useInfiniteNotificationsScroll( notificationParams );

const { loadTime } = usePerformance( {
isLoading: isInitialLoading
Expand All @@ -44,6 +51,7 @@ const NotificationsContainer = (): Node => {
const onRefresh = async () => {
setRefreshing( true );
await refetch();
if ( typeof ( onRefreshProp ) === "function" ) onRefreshProp( );
setRefreshing( false );
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,52 @@
// @flow

import NotificationsListItem from "components/Notifications/NotificationsListItem";
import type { ApiNotification } from "api/types";
import NotificationsListItem from "components/Notifications/NotificationsListItem.tsx";
import {
ActivityIndicator, Body2, CustomFlashList,
ActivityIndicator,
Body2,
CustomFlashList,
CustomRefreshControl,
InfiniteScrollLoadingWheel,
OfflineNotice, ViewWrapper
OfflineNotice
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React, { useCallback } from "react";
import { useCurrentUser, useTranslation } from "sharedHooks";
import type { Notification } from "sharedHooks/useInfiniteNotificationsScroll";

type Props = {
data: Object,
data: ApiNotification[],
isError?: boolean,
isFetching?: boolean,
isInitialLoading?: boolean,
isConnected: boolean,
onEndReached: Function,
reload: Function,
isConnected: boolean | null,
onEndReached: ( ) => void,
onRefresh: ( ) => void,
refreshing: boolean,
onRefresh: Function
reload: ( ) => void
};

interface RenderItemProps {
// It is used, not sure what the problem is
// eslint-disable-next-line react/no-unused-prop-types
item: Notification;
}

const NotificationsList = ( {
data,
isError,
isFetching,
isInitialLoading,
isConnected,
onEndReached,
onRefresh,
reload,
refreshing,
onRefresh
}: Props ): Node => {
refreshing
}: Props ) => {
const { t } = useTranslation( );
const user = useCurrentUser( );

const renderItem = useCallback( ( { item } ) => (
<NotificationsListItem item={item} />
const renderItem = useCallback( ( { item }: RenderItemProps ) => (
<NotificationsListItem notification={item} />
), [] );

const renderItemSeparator = ( ) => <View className="border-b border-lightGray" />;
Expand Down Expand Up @@ -98,20 +105,18 @@ const NotificationsList = ( {
);

return (
<ViewWrapper>
<CustomFlashList
ItemSeparatorComponent={renderItemSeparator}
ListEmptyComponent={renderEmptyComponent}
ListFooterComponent={renderFooter}
data={data}
estimatedItemSize={85}
keyExtractor={item => item.id}
onEndReached={onEndReached}
refreshing={isFetching}
renderItem={renderItem}
refreshControl={refreshControl}
/>
</ViewWrapper>
<CustomFlashList
ItemSeparatorComponent={renderItemSeparator}
ListEmptyComponent={renderEmptyComponent}
ListFooterComponent={renderFooter}
data={data}
estimatedItemSize={85}
keyExtractor={( item: ApiNotification ) => item.id}
onEndReached={onEndReached}
refreshing={isFetching}
refreshControl={refreshControl}
renderItem={renderItem}
/>
);
};

Expand Down
Loading
Loading