Skip to content

Commit

Permalink
✨ Add red dot for unseen notification (#393)
Browse files Browse the repository at this point in the history
  • Loading branch information
nwingt authored Jul 31, 2023
1 parent faf3b65 commit 24397ab
Show file tree
Hide file tree
Showing 17 changed files with 557 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ MIN_VERSION_FOR_WALLET=300

LIKECO_API_URL=https://like.co/api
LIKECOIN_API_URL=https://api.like.co
LIKECOIN_CHAIN_API_URL=https://mainnet-node.like.co
LIKECOIN_NFT_API_WALLET=like17m4vwrnhjmd20uu7tst7nv0kap6ee7js69jfrs
LIKERLAND_URL=https://liker.land
LIKERLAND_LOGIN_MESSAGE=Login
LIKECOIN_BUTTON_BASE_URL=https://button.like.co
SUPERLIKE_BASE_URL=https://s.like.co

Expand Down
1 change: 1 addition & 0 deletions app/components/main-tab-bar/main-tab-bar.props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface MainTabBarIconProps {
horizontal?: boolean
routeName: string
user?: User
unseenEventCount?: number
}
31 changes: 29 additions & 2 deletions app/components/main-tab-bar/main-tab-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as React from "react"
import { View } from "react-native"
import styled from "styled-components/native"
import { inject, observer } from "mobx-react"

import { MainTabBarIconProps } from "./main-tab-bar.props"
Expand All @@ -10,13 +12,27 @@ import { RootStore } from "../../models/root-store"

import { color } from "../../theme"

const NotificationIconWrapper = styled(View)`
position: relative;
`
const NotificationDot = styled(View)`
position: absolute;
top: 0;
right: 0;
width: 8;
height: 8;
borderRadius: 4;
background-color: #e35050;
`

@inject((rootStore: RootStore) => ({
user: rootStore.userStore.currentUser,
unseenEventCount: rootStore.userStore.unseenEventCount,
}))
@observer
export class MainTabBarIcon extends React.Component<MainTabBarIconProps> {
render() {
const { focused, routeName, user } = this.props
const { focused, routeName, user, unseenEventCount } = this.props
let name: IconTypes
const size = 24
switch (routeName) {
Expand Down Expand Up @@ -52,13 +68,24 @@ export class MainTabBarIcon extends React.Component<MainTabBarIconProps> {
}
}
const fill = focused ? color.palette.likeCyan : color.palette.lightGrey
return (
const icon = (
<Icon
name={name}
width={size}
height={size}
fill={fill}
/>
)

if (routeName === "Notification" && unseenEventCount > 0) {
return (
<NotificationIconWrapper>
<NotificationDot />
{icon}
</NotificationIconWrapper>
)
}

return icon
}
}
7 changes: 6 additions & 1 deletion app/components/nft-web-view/nft-web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const ControlBar = styled.View`
border-color: ${({ theme }) => theme.color.separator};
`

export function NFTWebView({ style, ...props }: WebViewProps) {
export interface NFTWebViewProps extends WebViewProps {
onPressRefresh?: () => void
}

export function NFTWebView({ style, onPressRefresh, ...props }: NFTWebViewProps) {
const webViewRef = React.useRef<WebViewBase>(null)

const [webViewKey, setWebViewKey] = React.useState(0);
Expand All @@ -46,6 +50,7 @@ export function NFTWebView({ style, ...props }: WebViewProps) {

const handleRefreshButtonPress = () => {
webViewRef.current?.reload()
onPressRefresh?.()
}

const handleHardwareBackButtonPress = () => {
Expand Down
3 changes: 3 additions & 0 deletions app/models/authcore-store/authcore-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export const AuthCoreStoreModel = types
)
self.profile = AuthCoreUserModel.create(currentUser)
}),
waitForInit: flow(function*() {
yield pendingInitPromise
}),
},
}
})
Expand Down
23 changes: 22 additions & 1 deletion app/models/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { AuthCoreAPI } from "../services/authcore"
import { CosmosAPI } from "../services/cosmos"
import { Mintscan } from "../services/mintscan"
import { Reactotron } from "../services/reactotron"
import { LikeCoAPI, LikeCoinAPI } from "../services/api"
import {
LikeCoAPI,
LikeCoinAPI,
LikeCoinChainAPI,
LikerLandAPI,
} from "../services/api"
import { BranchIO } from "../services/branch-io"
import { initSentry } from "../utils/sentry"

Expand All @@ -19,6 +24,8 @@ export class Environment {
this.authCoreAPI = new AuthCoreAPI()
this.likeCoAPI = new LikeCoAPI()
this.likeCoinAPI = new LikeCoinAPI()
this.likeCoinChainAPI = new LikeCoinChainAPI()
this.likerLandAPI = new LikerLandAPI()
this.cosmosAPI = new CosmosAPI()
this.mintscan = new Mintscan()
this.branchIO = new BranchIO()
Expand All @@ -36,6 +43,8 @@ export class Environment {
COSMOS_ADDRESS_PREFIX,
LIKECO_API_URL,
LIKECOIN_API_URL,
LIKECOIN_CHAIN_API_URL,
LIKERLAND_URL,
MINTSCAN_URL,
SENTRY_DSN,
SENTRY_ENV,
Expand All @@ -46,6 +55,8 @@ export class Environment {
this.authCoreAPI.setup(AUTHCORE_ROOT_URL, COSMOS_CHAIN_ID, COSMOS_ADDRESS_PREFIX)
this.likeCoAPI.setup(LIKECO_API_URL)
this.likeCoinAPI.setup(LIKECOIN_API_URL)
this.likeCoinChainAPI.setup(LIKECOIN_CHAIN_API_URL)
this.likerLandAPI.setup(LIKERLAND_URL)
this.cosmosAPI.setup(COSMOS_LCD_URL, this.appConfig.getGasLimits())
this.mintscan.setup(MINTSCAN_URL)
}
Expand Down Expand Up @@ -75,6 +86,16 @@ export class Environment {
*/
likeCoinAPI: LikeCoinAPI

/**
* LikeCoin chain API.
*/
likeCoinChainAPI: LikeCoinChainAPI

/**
* Like Land API.
*/
likerLandAPI: LikerLandAPI

/**
* AuthCore API.
*/
Expand Down
7 changes: 0 additions & 7 deletions app/models/root-store/setup-root-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@ export async function setupRootStore() {
// load data from storage
data = await storage.load(storageKey) || {}
rootStore = createRootStore(env, data)

if (rootStore.userStore.currentUser) {
rootStore.userStore.authCore.resume().then(() => {
const address = rootStore.userStore.authCore.primaryCosmosAddress
rootStore.chainStore.setupWallet(address)
})
}
} catch (e) {
// if there's any problems loading, then let's at least fallback to an empty state
// instead of crashing.
Expand Down
149 changes: 149 additions & 0 deletions app/models/user-store/user-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {
UserResult,
UserRegisterParams,
SuperLikeStatusResult,
LikerLandUserFolloweeListResult,
NFTClassListResult,
NFTEvent,
LikerLandUserInfoResult,
} from "../../services/api"

import { throwProblem } from "../../services/api/api-problem"
Expand All @@ -64,6 +68,7 @@ export const UserStoreModel = types
isSigningIn: false,
isSigningOut: false,
trackingStatus: "not-determined" as TrackingStatus,
unseenEventCount: 0,
}))
.extend(withEnvironment)
.views(self => ({
Expand Down Expand Up @@ -417,6 +422,150 @@ export const UserStoreModel = types
yield self.appMeta.postResume();
}),
}))
.actions(self => ({
loginLikerLand: flow(function * () {
yield self.authCore.waitForInit()
const address = self.authCore.primaryCosmosAddress
const signingPayload = {
/* eslint-disable @typescript-eslint/camelcase */
chain_id: self.env.appConfig.getValue('COSMOS_CHAIN_ID'),
memo: [
`${self.env.appConfig.getValue('LIKERLAND_LOGIN_MESSAGE')}:`,
JSON.stringify({
ts: Date.now(),
address,
}),
].join(' '),
msgs: [],
fee: {
gas: '0',
amount: [
{
denom: self.env.appConfig.getValue('COSMOS_FRACTION_DENOM'),
amount: '0',
},
],
},
sequence: '0',
account_number: '0',
/* eslint-enable @typescript-eslint/camelcase */
};
const {
signed,
signature: {
signature,
pub_key: publicKey
},
}: any = yield self.env.authCoreAPI.signAmino(signingPayload, address)
const data = {
signature,
publicKey: publicKey.value,
message: stringify(signed),
from: address,
signMethod: 'memo',
};
yield self.env.likerLandAPI.login(data)
}),
fetchLikerLandFollowees: flow(function * () {
const result: LikerLandUserFolloweeListResult = yield self.env.likerLandAPI.fetchUserFolloweeList()
switch (result.kind) {
case "ok": {
return result.data
}
default:
throwProblem(result)
return undefined
}
}),
}))
.actions(self => {
async function fetchFolloweeNewClassEvents(followee: string) {
const result: NFTClassListResult = await self.env.likeCoinChainAPI.fetchNFTClassList({
iscnOwner: followee,
reverse: true,
})
switch (result.kind) {
case "ok":
return result.data.map(({ id, created_at: timestamp }) => ({
/* eslint-disable @typescript-eslint/camelcase */
action: 'new_class',
class_id: id,
nft_id: '',
sender: followee,
receiver: '',
tx_hash: '',
timestamp,
memo: '',
/* eslint-enable @typescript-eslint/camelcase */
} as NFTEvent))

default:
return []
}
}

function getEventType(event: NFTEvent) {
const user = self.authCore.primaryCosmosAddress
let eventType;
if (event.action === "new_class") {
eventType = "mint_nft"
} else if (event.action === "buy_nft" || event.action === "sell_nft") {
if (event.receiver === user) {
eventType = "purchase_nft"
} else {
eventType = "nft_sale"
}
} else if (event.sender === self.env.appConfig.getValue('LIKECOIN_NFT_API_WALLET')) {
if (event.receiver === user) {
eventType = "purchase_nft"
} else {
eventType = "nft_sale"
}
} else if (event.receiver === user) {
eventType = "receive_nft"
} else if (event.sender === user) {
eventType = "send_nft"
} else {
eventType = "transfer_nft"
}
return eventType;
}

return {
loadUnseenEventCount: flow(function * () {
const userInfoResult: LikerLandUserInfoResult = yield self.env.likerLandAPI.fetchUserInfo()
if (userInfoResult.kind !== "ok") return

const { eventLastSeenTs } = userInfoResult.data

const { followees }: { followees: string[] } = yield self.fetchLikerLandFollowees();

const eventResponses: NFTEvent[][] = yield Promise.all([
self.env.likeCoinChainAPI.fetchNFTEvents({
involver: self.authCore.primaryCosmosAddress,
limit: 100,
actionType: ["/cosmos.nft.v1beta1.MsgSend", "buy_nft"],
ignoreToList: self.env.appConfig.getValue('LIKECOIN_NFT_API_WALLET'),
reverse: true,
}).then((result) => result.kind === 'ok' ? result.data : []).catch(() => []),
...followees.map(fetchFolloweeNewClassEvents)
])
const events = eventResponses.flat()
self.unseenEventCount = events.reduce((count, event) => {
if (
eventLastSeenTs < new Date(event.timestamp).getTime() &&
["nft_sale", "receive_nft", "mint_nft"].includes(getEventType(event))
) {
return count + 1
}
return count
}, 0)
}),
clearUnseenEventCount() {
self.unseenEventCount = 0
},
}
});

type UserStoreType = Instance<typeof UserStoreModel>
export interface UserStore extends UserStoreType {}
Expand Down
Loading

0 comments on commit 24397ab

Please sign in to comment.