diff --git a/src/common/components/profile-activities/activities-dropdown.tsx b/src/common/components/profile-activities/activities-dropdown.tsx new file mode 100644 index 00000000000..475305ee2b9 --- /dev/null +++ b/src/common/components/profile-activities/activities-dropdown.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react' +import DropDown from "../dropdown" +import { ActivitiesGroup } from "./types/activities-group" + +interface Props { + setFilter: (v: ActivitiesGroup | "") => void +} + +const ActivitiesDropdown = (props: Props) => { + + const { setFilter } = props; + + const [label, setLabel] = useState("") + + const dropDown = ( +
+
+ {(() => { + let dropDownConfig: any; + dropDownConfig = { + history: "", + label: label ? label : "All", + items: [ + { + label: All, + onClick: () => { + setLabel("All"); + setFilter(""); + } + }, + { + label: Comments, + onClick: () => { + setLabel("Comments"); + setFilter("comment"); + } + }, + { + label: Replies, + onClick: () => { + setLabel("Replies"); + setFilter("comment"); + } + }, + { + label: Custom json, + onClick: () => { + setLabel("Follows"); + setFilter("custom_json"); + } + }, + { + label: Witness votes, + onClick: () => { + setLabel("Witness votes"); + setFilter("account_witness_vote"); + } + }, + { + label: Proposal votes, + onClick: () => { + setLabel("Proposal votes"); + setFilter("update_proposal_votes"); + } + } + ] + }; + return ( +
+ +
+ ); + })()} +
+
+ ); + + return ( + <> +
+
Filter activities
+
+ {dropDown} + + ) +} + +export default ActivitiesDropdown \ No newline at end of file diff --git a/src/common/components/profile-activities/activities-types.tsx b/src/common/components/profile-activities/activities-types.tsx new file mode 100644 index 00000000000..db1ab50612c --- /dev/null +++ b/src/common/components/profile-activities/activities-types.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import { Account } from "../../store/accounts/types" +import { Global } from '../../store/global/types' + +interface Props { + account: Account; + global: Global; +} + +const ActivitiesTypes = (props: Props) => { + + const { account, global } = props; + return ( +
+
+
Activity Types
+
+
+
+ Comments + Replies + Votes + Communities +
+
+ Witness votes + Proposal votes +
+
+
+ ) +} + +export default ActivitiesTypes \ No newline at end of file diff --git a/src/common/components/profile-activities/activities.tsx b/src/common/components/profile-activities/activities.tsx new file mode 100644 index 00000000000..78a6cf766b9 --- /dev/null +++ b/src/common/components/profile-activities/activities.tsx @@ -0,0 +1,289 @@ +import React from 'react' +import { dateToFullRelative } from '../../helper/parse-date'; +import { Link } from 'react-router-dom'; +import { upvote, ticketSvg, starSvg, peopleSvg, commentSvg, chevronDownSvgForSlider } from '../../img/svg'; +import { Account } from '../../store/accounts/types'; +import { ActivityTypes } from './types/types'; +import { _t } from '../../i18n'; + +interface Props { + a: ActivityTypes; + account: Account; + jsonData: any; +} + +const UserActivities = (props: Props) => { + + const { a, account, jsonData } = props; + + return ( + <> + {a?.type === "comment" && a?.author === account!.name ? <> +
+
+
+ {commentSvg} +
+
+
+ {_t("profile-activities.comment")} + + {a.parent_permlink} + + {_t("profile-activities.by")} + + @{a.parent_author === "" ? a.author : a.parent_author} + +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : a?.type === "comment" && a?.author !== account?.name ? <> +
+
+
+ {commentSvg} +
+
+
+ @{a?.author} + {_t("profile-activities.replied")} + + {a.parent_permlink} + + {_t("profile-activities.by")} + + @{a.parent_author === "" ? a.author : a.parent_author} + +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : a?.type === "vote" ? <> +
+
+
+ {upvote} +
+
+
+ {_t("profile-activities.voted")} + + {a.permlink} + + {_t("profile-activities.by")} + @{a.author} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : a?.type === "proposal_pay" ? <> +
+
+
+ {ticketSvg} +
+
+
+ {_t("profile-activities.received", {n: a.payment} )} + @{a.payer} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.id === "follow" && jsonData[1]!?.what?.includes("blog")) ? <> +
+
+
+ {starSvg} +
+
+
+ {_t("profile-activities.follow")} + @{jsonData[1].following} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.id === "follow" && !jsonData[1]?.what?.includes("blog")) ? <> +
+
+
+ {starSvg} +
+
+
+ {_t("profile-activities.unfollow")} + @{jsonData[1].following} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.id === "community" && jsonData?.includes("subscribe")) ? <> +
+
+
+ {peopleSvg} +
+
+
+ {_t("profile-activities.subscribed")} + {jsonData[1]?.community} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + :( a?.id === "community" && jsonData?.includes("unsubscribe")) ? <> +
+
+
+ {peopleSvg} +
+
+
+ {_t("profile-activities.unsubscribed")} + {jsonData[1]?.community} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.type === "account_witness_vote" && a?.approve) ? <> +
+
+
+ {upvote} +
+
+
+ {_t("profile-activities.witness-vote")} + @{a.witness} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.type === "account_witness_vote" && !a?.approve) ? <> +
+
+
+
+ + {chevronDownSvgForSlider} + +
+
+
+
+ {_t("profile-activities.witness-unvote")} + @{a.witness} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.type === "update_proposal_votes" && a?.approve) ? <> +
+
+
+ {upvote} +
+
+
+ {_t("profile-activities.approved")} + proposal#{a.proposal_ids} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : (a?.type === "update_proposal_votes" && !a?.approve) ? <> +
+
+
+
+ + {chevronDownSvgForSlider} + +
+
+
+
+ {_t("profile-activities.unapproved")} + proposal#{a?.proposal_ids} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : a?.type === "account_update2" ? <> +
+
+
+ {starSvg} +
+
+
+ @{a?.account} + {_t("profile-activities.update")} +
+
+ {dateToFullRelative(a.timestamp)} +
+
+
+
+ + : <>} + + ) +} + +export default UserActivities; \ No newline at end of file diff --git a/src/common/components/profile-activities/index.scss b/src/common/components/profile-activities/index.scss new file mode 100644 index 00000000000..4cbb67468cb --- /dev/null +++ b/src/common/components/profile-activities/index.scss @@ -0,0 +1,159 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + +.activities-container{ + display: flex; + flex-direction: column; + gap: 10px; + + .activities-page-info{ + align-self: center; + } + + .activity-bottom-column{ + display: flex; + flex-direction: column-reverse; + @extend .activity-bottom-common; + + } + + .activities-bottom{ + display: flex; + width: 100%; + padding: 20px; + gap: 20px; + + @extend .activity-bottom-common; + } + +} + +.activity-bottom-common{ + .activities-filter{ + position: sticky; + flex: 1; + + .filter-dropdown{ + display: flex; + flex-direction: column; + padding: 5px; + margin-bottom: 15px; + + @include themify(day) { + border-bottom: 1px solid #f5f5f5; + } + + @include themify(night) { + border-bottom: 1px solid #161d26; + } + + .dropdown-header{ + align-self: center; + } + } + + .types-container{ + display: flex; + flex-direction: column; + + .types-header{ + align-self: center; + } + + .types-wrapper{ + padding: 5px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .filter-types{ + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + + @include themify(day) { + border-bottom: 1px solid #f5f5f5; + } + + @include themify(night) { + border-bottom: 1px solid #161d26; + } + } + } + + } + } + + .activities-wrapper{ + flex: 4; + + .activities{ + display: flex; + flex-direction: column; + justify-content: center; + gap: 3px; + // width: 75%; + height: fit-content; + padding: 10px; + border-radius: 8px; + + &:nth-child(odd) { + @include themify(day) { + background: $white-three; + } + + @include themify(night) { + background: $dark-two; + } + } + + .activities-header{ + justify-self: center; + align-self: center; + font-size: 14px; + } + + .activities-info-wrapper{ + display: flex; + gap: 10px; + + .activities-body{ + flex: 2; + } + + .activities-details{ + flex: 1; + display: flex; + align-items: center; + gap: 15px; + + .activity-icon{ + display: flex; + width: 20px; + + .downvote-icon{ + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + height: 20px; + width: 20px; + color: red; + border: 1px solid red; + } + } + + .activity-info{ + display: flex; + flex-direction: column; + gap: 5px; + } + } + } + + } + } +} \ No newline at end of file diff --git a/src/common/components/profile-activities/index.tsx b/src/common/components/profile-activities/index.tsx new file mode 100644 index 00000000000..24fbb218944 --- /dev/null +++ b/src/common/components/profile-activities/index.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useState } from "react" +import "./index.scss" +import { Link } from "react-router-dom" +import LinearProgress from "../linear-progress" +import { Account } from "../../store/accounts/types" +import { Button } from "react-bootstrap" +import { _t } from "../../i18n" +import UserActivities from "./activities" +import ActivitiesDropdown from "./activities-dropdown" +import ActivitiesTypes from "./activities-types" +import { ActivityTypes } from './types/types'; +import { fetchActvities } from "./operations" +import { ActivitiesGroup } from "./types/activities-group" +import { Global } from "../../store/global/types"; + +interface Props{ + account: Account; + global: Global; +} + +export const ProfileActivites = (props: Props) => { + + const { account, global } = props; + + const [activities, setActivities] = useState([]); + const [filteredActivities, setFilteredActivities] = useState([]); + const [loading, setLoading] = useState(false); + const [filter, setFilter] = useState(""); + const [isFiltered, setIsFiltered] = useState(false); + const [activityId, setActivityId] = useState("") + + useEffect(() => { + userActivities(-1); + },[filter]); + + const userActivities = async (start: number) => { + setLoading(true); + + try { + const data = await fetchActvities(account!.name, filter, start, 20); + const filterNotifications = data?.filter((a: ActivityTypes) => a?.id !== "notify" && a?.id !== "ecency_notify"); + + if (filterNotifications?.length > 0) { + setActivityId(filterNotifications[0].trx_id); + if (!filter) { + setIsFiltered(false) + setActivities(prevActivities => [...prevActivities, ...filterNotifications]); + } else { + setIsFiltered(true) + + const filterNewActivities = [...filteredActivities, ...filterNotifications].filter((a: ActivityTypes) => a.type === filter) + setFilteredActivities(filterNewActivities); + } + } + + setLoading(false); + } catch (err) { + console.log(err); + setLoading(false); + } + }; + + const handleLoadMore = () => { + const lastActivity = activitiesToMap[activitiesToMap?.length - 1]; + if (canLoadMore) { + userActivities(lastActivity.num); + } + }; + + const handleCustomJson = (a: ActivityTypes) => { + let jsonData; + try { + if(a?.json){ + jsonData = JSON.parse(a?.json) + }; + } catch (error) { + console.log(error) + } + return jsonData + }; + + const activitiesToMap: ActivityTypes[] = !isFiltered ? activities : filteredActivities + const canLoadMore = activityId !== activitiesToMap[activitiesToMap?.length - 1]?.trx_id + + return ( + <> + {loading && } +
+
+ Activities related to @{account?.name}'s account +
+
+
+ {activitiesToMap?.map((a: ActivityTypes, i: number) => { + const jsonData = handleCustomJson(a) + return ( +
+ +
+ )})} + {!loading &&
+ +
} +
+
+
+ +
+
+ +
+
+
+
+ + ) +} diff --git a/src/common/components/profile-activities/operations.ts b/src/common/components/profile-activities/operations.ts new file mode 100644 index 00000000000..5875fadbad3 --- /dev/null +++ b/src/common/components/profile-activities/operations.ts @@ -0,0 +1,95 @@ +import { utils } from "@hiveio/dhive"; +import { getAccountHistory } from "../../api/hive"; +import { ActivitiesGroup } from "./types/activities-group"; + +const ops = utils.operationOrders; + +export const ACCOUNT_ACTIVITY_GROUPS: Record = { + comment: [ + ops.comment + ], + proposal_pay: [ + ops.proposal_pay + ], + vote: [ + ops.vote + ], + custom_json: [ + ops.custom_json, + ], + account_witness_vote: [ + ops.account_witness_vote + ], + update_proposal_votes: [ + ops.update_proposal_votes + ], + update: [ + ops.account_update2 + ] + }; + + const ALL_ACCOUNT_OPERATIONS = [...Object.values(ACCOUNT_ACTIVITY_GROUPS)].reduce( + (acc, val) => acc.concat(val), + [] + ); + + export const fetchActvities = (username: string, group: ActivitiesGroup | "" = "", start: number = -1, limit: number = 20) => { + + const name = username.replace("@", ""); + + let filters: ActivitiesGroup[] | "" = []; + switch (group) { + case "comment": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["comment"]); + break; + case "proposal_pay": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["proposal_pay"]); + break; + case "vote": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["vote"]); + break; + case "custom_json": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["custom_json"]); + break; + case "account_witness_vote": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["account_witness_vote"]); + break; + case "update_proposal_votes": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["update_proposal_votes"]); + break; + case "update": + filters = utils.makeBitMaskFilter(ACCOUNT_ACTIVITY_GROUPS["update"]); + break; + default: + filters = utils.makeBitMaskFilter(ALL_ACCOUNT_OPERATIONS); + } + + const res = getAccountHistory(name, filters, start, limit) + .then((r) => { + const mapped: any = r.map((x: any) => { + const { op } = x[1]; + const { timestamp, trx_id } = x[1]; + const opName = op[0]; + const opData = op[1]; + + return { + num: x[0], + type: opName, + timestamp, + trx_id, + ...opData + }; + }); + + const activities = mapped + .filter((x: any) => x !== null) + .sort((a: any, b: any) => b.num - a.num); + + return activities + + }) + .catch((err) => { + console.log("catch", err); + }); + return res + }; \ No newline at end of file diff --git a/src/common/components/profile-activities/types/activities-group.ts b/src/common/components/profile-activities/types/activities-group.ts new file mode 100644 index 00000000000..dbab55fd123 --- /dev/null +++ b/src/common/components/profile-activities/types/activities-group.ts @@ -0,0 +1,8 @@ +export type ActivitiesGroup = + | "comment" + | "vote" + | "proposal_pay" + | "account_witness_vote" + | "custom_json" + | "update_proposal_votes" + | "update" \ No newline at end of file diff --git a/src/common/components/profile-activities/types/types.ts b/src/common/components/profile-activities/types/types.ts new file mode 100644 index 00000000000..78d52944aff --- /dev/null +++ b/src/common/components/profile-activities/types/types.ts @@ -0,0 +1,22 @@ +export interface ActivityTypes { + type: string; + parent_author?: string; + parent_permlink?: string; + author?: string; + permlink?: string; + payment?: number; + payer?: string; + following?: string; + community?: string; + approve?: boolean; + proposal_ids?: string; + account?: string; + id?: string; + what?: string[]; + voter?: string; + witness?: string; + json: string; + timestamp: string; + num: number; + trx_id: string; + } \ No newline at end of file diff --git a/src/common/components/profile-menu/index.tsx b/src/common/components/profile-menu/index.tsx index e39f1c079ca..624e90fabca 100644 --- a/src/common/components/profile-menu/index.tsx +++ b/src/common/components/profile-menu/index.tsx @@ -32,7 +32,7 @@ export class ProfileMenu extends Component { render() { const { username, section, activeUser } = this.props; - const kebabMenuItems: MenuItem[] = ["trail", "replies", "communities"].map((x) => { + const kebabMenuItems: MenuItem[] = ["trail", "replies", "communities", "comments"].map((x) => { return { label: _t(`profile.section-${x}`), href: `/@${username}/${x}`, @@ -49,7 +49,7 @@ export class ProfileMenu extends Component { }; const menuItems: MenuItem[] = [ - ...[ProfileFilter.blog, ProfileFilter.posts, ProfileFilter.comments].map((x) => { + ...[ProfileFilter.blog, ProfileFilter.posts, ProfileFilter.activities].map((x) => { return { label: _t(`profile.section-${x}`), href: `/@${username}/${x}`, @@ -66,7 +66,8 @@ export class ProfileMenu extends Component { } = { history: this.props.history, label: ProfileFilter[section] ? _t(`profile.section-${section}`) : "", - items: [...menuItems, ...kebabMenuItems.filter((item) => item.selected)] + items: [...menuItems] + // items: [...menuItems, ...kebabMenuItems.filter((item) => item.selected)] }; const dropDownMenuItems: MenuItem[] = [ @@ -157,9 +158,9 @@ export class ProfileMenu extends Component { {_t(`profile.section-settings`)} )} -
+ {/*
-
+
*/}
diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 79fb6d7da75..2774922350b 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -849,6 +849,7 @@ "section-blog": "Blog", "section-posts": "Posts", "section-trail": "Likes", + "section-activities": "Activites", "section-comments": "Comments", "section-replies": "Replies", "section-communities": "Communities", @@ -2123,5 +2124,21 @@ "insufficient-resource-buy-hive": "Buy HIVE and Power it up", "insufficient-resource-wait": "Wait few hours for RC refill", "report": "Report" + }, + "profile-activities": { + "by": "by", + "comment": "commented on", + "replied": "replied to", + "voted": "voted on", + "received": "received {{n}} from", + "follow": "started following", + "unfollow": "unfollowed", + "subscribed": "subscribed to community", + "unsubscribed": "unsubscribed from community", + "witness-vote": "voted witness", + "witness-unvote": "unvoted witness", + "approved": "approved", + "unapproved": "unapproved", + "update": "updated thier account" } } diff --git a/src/common/pages/profile-functional.tsx b/src/common/pages/profile-functional.tsx index 3f81b2bc045..726ba2885d7 100644 --- a/src/common/pages/profile-functional.tsx +++ b/src/common/pages/profile-functional.tsx @@ -52,6 +52,7 @@ import WalletSpk from "../components/wallet-spk"; import "./profile.scss"; import { useQueryClient } from "@tanstack/react-query"; import { QueryIdentifiers } from "../core"; +import { ProfileActivites } from "../components/profile-activities"; interface MatchParams { username: string; @@ -437,7 +438,7 @@ export const Profile = (props: Props) => {
{ProfileMenu({ ...props, username, section })} - {[...Object.keys(ProfileFilter), "communities"].includes(section) && + {[...Object.keys(ProfileFilter), "communities"].includes(section) && section !== "activities" && ProfileCover({ ...props, account })} {data && @@ -585,6 +586,23 @@ export const Profile = (props: Props) => { ); } + if (section === "activities") { + return ( + <> +
+
+ +
+
+ + ); + } if (data !== undefined && section) { let entryList; diff --git a/src/common/routes.ts b/src/common/routes.ts index b79f4df04a3..ce2d14283d2 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -24,7 +24,7 @@ export default { USER_FEED: `/:username(@[\\w\\.\\d-]+)/:section(feed)`, USER_SECTION: `/:username(@[\\w\\.\\d-]+)/:section(${profileFilters.join( "|" - )}|wallet|points|engine|communities|settings|permissions|referrals|followers|following|spk|trail)`, + )}|wallet|points|engine|communities|settings|permissions|referrals|followers|following|spk|trail|activities)`, COMMUNITIES: `/communities`, COMMUNITIES_CREATE: `/communities/create`, COMMUNITIES_CREATE_HS: `/communities/create-hs`, diff --git a/src/common/store/global/types.ts b/src/common/store/global/types.ts index b4c2a58b910..25b5a750475 100644 --- a/src/common/store/global/types.ts +++ b/src/common/store/global/types.ts @@ -26,6 +26,7 @@ export enum EntryFilter { export enum ProfileFilter { blog = "blog", posts = "posts", + activities = "activities", comments = "comments", replies = "replies" }