Docyrus

ContactActivityPanel

Typed activity timeline for a contact or record, with category tabs, a type filter, relationship summary, upcoming tasks, an emit-only Add menu and a detail sheet with replies and reactions.

iOSAndroid
Preview ContactActivityPanel on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-contact-activity-panel
Required Packages(4 packages)
pnpm add @shopify/flash-list @tanstack/react-query react-native-reanimated react-native-gesture-handler

The panel stacks these regions:

  1. Header (contact, showHeader, headerActions).
  2. Category tabs (All / Communication / Tasks / Updates) with counts, plus a filter button that opens the grouped type filter in an action sheet.
  3. Summary row: last contacted, next activity, open task count, the Ask Docy button (renderAiAssistant) and the + Add menu (addActions + onAddActivity).
  4. Upcoming tasks and meetings. Task checkboxes call onToggleUpcomingTask.
  5. Timeline: calendar day groups (Today, Yesterday, This week, Last week, This month, Last month, Earlier this year, Last year, Older) on a continuous rail. Communication types render as cards (title, preview, time range, attachment and reply counts, participant avatars). Tasks and updates render as one-line rows with an attachment marker. The list is virtualised with FlashList.
  6. Detail sheet (large action sheet): type chip, time range and full timestamp, a Details block of the activity's own metadata (call direction / duration, meeting location, email recipients, task status / priority / due date, …), participants, body, summary, key points, follow-ups (onToggleFollowUp), document cards (image thumbnails, per-type icons, tap to open), related records, a reaction row, the reply thread for comments, the primary action and Delete.

The Add menu emits onAddActivity(type). Pass renderAddForm as well and the panel opens your form for the picked type in its own sheet — typically a LogActivityForm scoped with activityTypes={[type]}. Call close() after a successful submit. With a single addActions entry the trigger skips the menu and opens the form directly.

Timestamps

Timeline timestamps count up while the event is recent and become a date once it isn't: now → 5m → 3h → 2d → 3w → 21 Sep → 21 Sep 2025. The units are translation keys (ui.contactActivity.time.*) and the absolute form uses the device locale.

Connect it to Docyrus with useDocyrusContactActivity, which is also re-exported from this component.

Usage

import { ContactActivityPanel, useDocyrusContactActivity } from '@/components/docyrus-native/contact-activity-panel';

function ContactTimeline({ client, contact, meId }) {
  const [logType, setLogType] = useState<string | null>(null);
  const { panelProps } = useDocyrusContactActivity({
    client,
    appSlug: 'base',
    dataSourceSlug: 'contact',
    recordId: contact.id,
    relationSlug: 'contact',
    currentUserId: meId
  });

  return (
    <ContactActivityPanel
      {...panelProps}
      contact={{ name: contact.name, org: contact.company }}
      addActions={[
        { type: 'call', label: 'Log call', icon: 'fal phone' },
        { type: 'meeting', label: 'Log meeting', icon: 'fal calendar' },
        { type: 'task', label: 'New task', icon: 'fal square-check' }
      ]}
      onAddActivity={setLogType} />
  );
}

Without maxHeight the panel is flex-1 and fills its parent, so give the parent a bounded height (a screen, or a sized View). Do not nest it in a vertical ScrollView. Pass maxHeight for a fixed-height panel.

Replies and reactions

For comment activities the detail sheet loads replies with onLoadReplies(activityId) and renders them with the team-chat PostCard / PostComposer (as on web): @ mentions, emoji, attachments and — with dataSources + onSearchEntity — entity links. Submitting calls onCreateComment({ content, parent_id, attachments, linked_entities, mentioned_user_ids, hashtags }). parent_id is the raw comment id taken from metadata.__sourceId. Own replies can be deleted after confirmation (onDeleteComment).

With onToggleReaction, the sheet shows reaction chips for the activity and for each reply. Tap the smiley / + button, or long-press a reply, to pick one of the preset emoji (or More for the full EmojiPicker). targetId is the reply id, or the activity's metadata.__sourceId (falling back to its id).

Editing replies is not offered (the panel has no update callback — same as web).

API Reference

ContactActivityPanelProps

PropTypeDefaultDescription
activitiesContactActivity[]-Timeline items. Sorted newest first by created_on (required)
currentUserChatUser-Signed-in user (own replies, delete permission)
usersChatUser[]-Users for avatars, names and mentions
contactNamestringcontact.nameName used in the empty state
isLoadingbooleanfalseShow the loading skeleton
isCreatePendingbooleanfalsePending state of the reply composer
isDeletePendingbooleanfalsePending state of the delete confirmations
onDeleteActivity(activityId: string) => void | Promise<void>-Enables Delete (after confirmation) on the current user's activities
onCreateComment(payload: CreatePostPayload) => void | Promise<void>-Post a reply to a comment activity
onDeleteComment(commentId: string) => void | Promise<void>-Delete an own reply
onToggleReaction(targetId: string, emoji: string) => void | Promise<void>-Toggle a reaction on an activity or reply
onUploadFile(file: NativeFile) => Promise<PostAttachment>-Accepted for API parity. Picked files arrive through onCreateComment (attachments)
onLoadReplies(activityId: string) => Promise<ChatPost[]>-Load a comment activity's replies
dataSourcesDataSourceOption[]-Data sources offered by the reply composer's link button (needs onSearchEntity)
onSearchEntity(dataSourceId: string, query: string) => Promise<EntitySearchResult[]>-Record search for linking an entity to a reply
activityTypesActivityType[]all 18Types offered in the filter and shown in the timeline
maxHeightnumber-Fixed panel height in dp. Without it the panel is flex-1
classNamestring-Container classes
contactContactActivityContact-Header identity
showHeaderbooleantrueShow the header (needs contact)
headerActionsReactNode-Slot on the right of the header
width'panel' | 'wide''panel'Accepted for API parity. Native always uses the compact panel layout
iconStyle'color' | 'mono''color'Colored or monochrome type chips
relationshipSummaryRelationshipSummary-Summary row values
upcomingUpcomingActivity[]-Upcoming tasks and meetings
onToggleUpcomingTask(id: string) => void | Promise<void>-Toggle an upcoming task's checkbox
upcomingCollapsedCountnumber4Upcoming entries shown before the "N more" control
weekStartsOnnumber1First day of the week for the timeline day groups (0 = Sunday)
addActionsAddActivityAction[]-Add menu entries (menu shows with onAddActivity or renderAddForm)
onAddActivity(type: string) => void-Fired when an Add menu entry is picked
renderAddForm(context: AddFormContext) => ReactNode-Quick-add form for the picked type, opened in its own sheet
onActivityClick(activity: ContactActivity) => void-Replace the built-in detail sheet
renderActivityDetail(activity: ContactActivity) => ReactNode-Replace the detail sheet body (header kept)
onToggleFollowUp(activityId: string, followUpId: string) => void | Promise<void>-Toggle a follow-up checkbox in the detail sheet
renderAiAssistant(context: { open: boolean; onOpenChange: (open: boolean) => void }) => ReactNode-Ask Docy slot. The button shows only when this is set. Render your own sheet
aiAssistantOpenboolean-Controlled Ask Docy open state
onAiAssistantOpenChange(open: boolean) => void-Ask Docy open-state callback

ContactActivity

FieldTypeDescription
idstringActivity id
typeActivityTypeOne of 18 types (see below)
subjectstringSubject (title fallback)
descriptionstringBody. Rendered as markdown in the detail sheet
metadataRecord<string, unknown>Source data (__sourceId, done, priority, …)
reply_countnumberReply count on the card
created_onstringISO timestamp (sort + date group)
created_bystringAuthor user id
last_modified_onstringISO timestamp
attachmentsPostAttachment[] | nullAttached files
reactionsPostReaction[]Reactions
titlestringDisplay title (falls back to subject)
previewstringOne-line preview (falls back to the first line of description)
participantsstring[]Extra participant user ids for the avatar stack
timeRange{ start: string; end?: string }Time range chip (14:00–14:45)
dueDatestringDue date (tasks)
actorLabelstringActor shown on secondary rows
summarystringDetail sheet summary
keyPointsstring[]Detail sheet key points
followUps{ id: string; text: string; done?: boolean }[]Detail sheet follow-ups
relatedActivityRelatedEntity[]Detail sheet related records (filled by the hook, or supplied here)
primaryAction{ label: string; onClick?: () => void }Detail sheet primary button

ActivityType: call, meeting, email, chat, sms, whatsapp, linkedin, note, comment (communication, rendered as cards), task (tasks), status_update, status, file, form, record, trace, record_create, record_update (updates). task, file and form rows open the detail sheet; other update rows are inert.

UpcomingActivity

FieldTypeDescription
idstringItem id
kind'task' | 'meeting'Tasks get a checkbox, meetings a calendar chip
titlestringTitle
substringSecondary line
duestringDisplay label for the due time
donebooleanTask done state

RelationshipSummary

FieldTypeDescription
lastContactedAtstringISO, shown relative ("3d ago")
nextActivityAtstringISO, shown as a date
openTaskCountnumberOpen task count

ContactActivityContact

FieldTypeDescription
namestringContact name
orgstringOrganization
avatarUrlstringAvatar image
initialsstringFallback initials

AddActivityAction

FieldTypeDescription
typestringPassed to onAddActivity
labelstringMenu label
iconstringDocyrusIcon name (for example fal phone). Web takes a lucide component

ActivityRelatedEntity

FieldTypeDescription
idstringRelated record id
labelstringDisplay label
rolestringRelation the entity came from ("Assignee", "Project")
onClick() => voidOpens the record; the chip is inert without it

AddFormContext

FieldTypeDescription
typestringThe AddActivityAction.type the user picked
close() => voidCloses the quick-add sheet

ChatUser / ChatPost / CreatePostPayload

TypeFields
ChatUserid, firstname?, lastname?, avatar_url?
ChatPostid, content, attachments, reactions, link_previews, linked_entities, hashtags, mentioned_user_ids, reply_count, created_on, created_by, last_modified_on, parent_id
CreatePostPayloadcontent, parent_id?, attachments?: NativeFile[], linked_entities?, hashtags?, mentioned_user_ids?
PostAttachmentid, file_name, file_type, file_size, signed_url
PostReactionemoji, user_ids, count

Components

ComponentDescription
ContactActivityPanelThe full panel

Exports

ExportDescription
ACTIVITY_CATEGORIES['communication', 'tasks', 'updates']
FILTER_GROUPSGrouped type list used by the filter sheet
getActivityCategory(type)Category of a type
isPrimaryActivity(type)Whether a type renders as a card
formatActivityTimestamp(value, t)Relative-then-absolute timeline timestamp
formatActivityTimestampFull(value)Full localized date + time
getActivityDetailRows(activity, t)The activity's metadata as label/value rows
groupActivitiesByDate(activities, weekStartsOn?)Calendar day groups
useDocyrusContactActivityDocyrus data hook (see its page)

Type Exports

TypeDescription
ContactActivityPanelPropsProps for ContactActivityPanel
ContactActivityTimeline item
ActivityType / ActivityCategoryType and category unions
UpcomingActivityUpcoming item
RelationshipSummarySummary row values
ContactActivityContactHeader identity
AddActivityActionAdd menu entry
AddFormContext{ type, close } handed to renderAddForm
ActivityRelatedEntityRelated record / person chip
ActivityDetailRowOne label/value line of the detail sheet's Details block
DateGroupKeyTimeline day-group key
ActivityTypeConfigPer-type icon, label, category, colors
ChatUser, ChatPost, PostAttachment, PostReaction, CreatePostPayload, LinkedEntity, EntitySearchResult, DataSourceOptionReply-thread types
ContactActivityClient, ContactActivitySources, EventTypeNameHints, UpdateStatusInput, ContactActivityPanelData, UseDocyrusContactActivityOptions, UseDocyrusContactActivityResultHook types (re-exported)

Translation Keys

KeyEnglish fallback
ui.contactActivity.titleActivity
ui.contactActivity.filterActivitiesFilter activities
ui.contactActivity.selectAllSelect all
ui.contactActivity.category.<all|communication|tasks|updates>All / Communication / Tasks / Updates
ui.contactActivity.filterGroup.<category>Filter group heading
ui.contactActivity.type.<activityType>Per-type label
ui.contactActivity.group.<today|yesterday|thisWeek|lastWeek|thisMonth|lastMonth|thisYear|lastYear|older>Day-group heading
ui.contactActivity.time.nownow
ui.contactActivity.time.minutes{{count}}m
ui.contactActivity.time.hours{{count}}h
ui.contactActivity.time.days{{count}}d
ui.contactActivity.time.weeks{{count}}w
ui.contactActivity.contactedContacted
ui.contactActivity.nextNext
ui.contactActivity.openTasksopen task(s)
ui.contactActivity.askDocyAsk Docy
ui.contactActivity.add / ui.contactActivity.addActivityAdd / Add activity
ui.contactActivity.upcomingUpcoming
ui.contactActivity.showMoreUpcomingShow {{count}} more upcoming
ui.contactActivity.showLessShow less
ui.contactActivity.detailsDetails
ui.contactActivity.detail.direction / .inbound / .outboundDirection / Inbound / Outbound
ui.contactActivity.detail.duration / .minutesDuration / {{count}} min
ui.contactActivity.detail.phone / .location / .eventType / .attendeesPhone / Location / Event type / Attendees
ui.contactActivity.detail.to / .from / .channelTo / From / Channel
ui.contactActivity.detail.status / .priority / .dueDateStatus / Priority / Due date
ui.contactActivity.detail.fileName / .fileType / .record / .urlFile / Type / Record / URL
ui.contactActivity.detail.assigneeAssignee (filled by the hook)
ui.contactActivity.documentsDocuments
ui.contactActivity.openAttachmentOpen {{name}}
ui.contactActivity.attachmentCount{{count}} attachment(s)
ui.contactActivity.relatedRelated
ui.contactActivity.summary / .keyPoints / .followUpsSummary / Key points / Follow-ups
ui.contactActivity.replies / .loadingReplies / .writeReplyReplies / Loading replies… / Write a reply...
ui.contactActivity.addReactionAdd reaction (native-only)
ui.contactActivity.deleteActivityDelete activity
ui.contactActivity.loadingActivitiesLoading activities…
ui.contactActivity.noActivitiesYetNo activities yet
ui.contactActivity.activityWithContactActivity with {{name}} will appear here.
ui.contactActivity.activityStartedLog a call, meeting, or email to get started.

Breaking changes

The panel was rewritten as the web typed timeline.

  • ActivityItem { id, type, title, description?, timestamp, user, metadata? } was replaced by ContactActivity (subject, created_on, created_by resolved against users, reply_count, …).
  • ActivityType grew from 6 to 18 members. status_change was removed. Use status_update or status.
  • The built-in "Add activity" form was removed. onAddActivity is now emit-only, (type: string) => void, instead of ({ type, title, description }). Open a LogActivityForm from it.
  • emptyMessage and style were removed. The empty state is translated (ui.contactActivity.*) and uses contactName. Style the panel with className.
  • Without maxHeight the panel is flex-1 and must not be nested in a vertical ScrollView.

On this page