Skip to content

Commit a6bd104

Browse files
[feature] Background message update and unread states (#178)
## Summary - Close #169, now it updates threads more effectively to avoid wasting data - Close #156, used the same `read_status` field on threads as Python client to determine unread state on initial fetch - Added threads attribute data for future reference - Renamed `designs` to `docs`
1 parent 6f7a571 commit a6bd104

File tree

9 files changed

+227
-1
lines changed

9 files changed

+227
-1
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
[
2+
"thread_id",
3+
"icebreakers",
4+
"snippet",
5+
"dismiss_inbox_nudge",
6+
"should_upsell_nudge",
7+
"public_chat_metadata",
8+
"is_creator_thread",
9+
"is_business_thread",
10+
"account_warning",
11+
"event_thread_metadata",
12+
"group_profile_id",
13+
"ctd_outcome_upsell_setting",
14+
"ctd_in_thread_upsell_insights",
15+
"takedown_data",
16+
"is_xac_readonly",
17+
"creator_agent_enabled",
18+
"read_receipts_disabled",
19+
"unpublished_pro_page_id",
20+
"live_location_session_id",
21+
"is_pin",
22+
"pinned_timestamp",
23+
"instamadillo_cutover_metadata",
24+
"is_3p_api_user",
25+
"typing_indicator_disabled",
26+
"locked_status",
27+
"notification_preview_controls",
28+
"universal_business_consent_settings",
29+
"customer_details",
30+
"recurring_prompt_type",
31+
"snoozed_messages_metadata",
32+
"is_verified_thread",
33+
"last_mentioned_item_timestamp_us",
34+
"lightweight_intervention_appealable_entity_id",
35+
"nudge",
36+
"nicknames",
37+
"reachability_status",
38+
"nicknames_setting",
39+
"participant_requests_count",
40+
"is_open_group_invite_thread",
41+
"scheduled_message_count",
42+
"must_show_in_thread_business_disclaimer",
43+
"ai_agent_voice_calling_enabled",
44+
"ai_agent_remixable",
45+
"recent_creation_time",
46+
"should_show_safety_card",
47+
"has_epd_restricted_user",
48+
"is_new_friend_bump",
49+
"pinned_activity",
50+
"ai_agent_visibility_status",
51+
"hidden_chat_info",
52+
"is_group_readd_request",
53+
"thread_title",
54+
"thread_label",
55+
"thread_languages",
56+
"is_group",
57+
"is_spam",
58+
"spam",
59+
"users",
60+
"shh_mode_enabled",
61+
"canonical",
62+
"relevancy_score",
63+
"relevancy_score_expr",
64+
"is_translation_enabled",
65+
"last_activity_at",
66+
"last_non_sender_item_at",
67+
"marked_as_unread",
68+
"approval_required_for_new_members",
69+
"assigned_admin_id",
70+
"admin_user_ids",
71+
"ongoing_call_timestamp_ms",
72+
"pinned_messages_metadata",
73+
"ad_context_data",
74+
"dm_settings",
75+
"label_items",
76+
"shh_transport_mode",
77+
"shh_toggler_userid",
78+
"messaging_thread_key",
79+
"has_newer",
80+
"has_older",
81+
"next_cursor",
82+
"prev_cursor",
83+
"policy_violation",
84+
"theme",
85+
"thread_context_items",
86+
"professional_metadata",
87+
"pending",
88+
"pending_user_ids",
89+
"last_seen_at",
90+
"smart_suggestion",
91+
"system_folder",
92+
"persistent_menu_icebreakers",
93+
"thread_has_audio_only_call",
94+
"is_xac_thread",
95+
"is_fanclub_subscriber_thread",
96+
"inviter",
97+
"input_mode",
98+
"thread_v2_id",
99+
"has_reached_message_request_limit",
100+
"last_mentioned_item_id",
101+
"thread_type",
102+
"thread_subtype",
103+
"btv_enabled_map",
104+
"translation_banner_impression_count",
105+
"read_state",
106+
"business_thread_folder",
107+
"is_creator_subscriber_thread",
108+
"group_link_joinable_mode",
109+
"joinable_group_link",
110+
"folder",
111+
"encoded_server_data_info",
112+
"e2ee_cutover_status",
113+
"left_users",
114+
"is_appointment_booking_enabled",
115+
"muted",
116+
"mentions_muted",
117+
"tq_seq_id",
118+
"named",
119+
"is_close_friend_thread",
120+
"vc_muted",
121+
"uq_seq_id",
122+
"video_call_id",
123+
"viewer_id",
124+
"archived",
125+
"bc_partnership",
126+
"shh_replay_enabled",
127+
"has_shared_account_participant",
128+
"other_participant_followers_10k_plus",
129+
"is_top_account_thread",
130+
"has_shared_account_participant_with_messaging_access",
131+
"is_stale",
132+
"ig_thread_capabilities",
133+
"oldest_cursor",
134+
"newest_cursor",
135+
"last_permanent_item",
136+
"items",
137+
"thread_image",
138+
"pals_feature_status",
139+
"direct_story"
140+
]

instagram-ts/docs/api-debugging.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# API Debugging Guide
2+
3+
In the past we used to go to a python / node shell, write code to call instagram-private-api methods, and inspect the responses to understand how the API works. Thanks to @qumisagi's contribution, we now have a more structured way to debug and understand the Instagram API flows.
4+
5+
## Property Tracker
6+
7+
The property tracker automatically discovers and logs new properties from Instagram API responses, helping you track schema changes over time.
8+
9+
### Usage
10+
11+
Import and call `registerProperties()` with an API response object and a schema filename:
12+
13+
```typescript
14+
import registerProperties from './utils/property-tracker.js';
15+
16+
// In your API call handler
17+
const response = await client.getThreads();
18+
if (response.length > 0) {
19+
registerProperties(response[0] as Record<string, any>, 'thread-schema.json');
20+
}
21+
```
22+
23+
### How it works
24+
25+
1. Extracts all top-level property names from the object
26+
2. Compares with previously registered properties (stored in `data/[filename]`)
27+
3. Logs new properties to console: `New properties found: prop1, prop2, ...`
28+
4. Appends new properties to the schema file for future reference
29+
30+
### Schema Files Location
31+
32+
Tracked schemas are stored in `/instagram-ts/data/`:
33+
34+
- `thread-schema.json` - Thread object properties
35+
- `message-schema.json` - Message object properties
36+
- etc.
37+
38+
### Example Output
39+
40+
```json
41+
New properties found: read_state, viewer_id, has_newer
42+
```
43+
44+
The schema file `thread-schema.json` gets updated to preserve the discovered properties for documentation.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

instagram-ts/source/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,8 @@ export class InstagramClient extends EventEmitter {
423423
users: this.getThreadUsers(thread),
424424
lastMessage: this.getLastMessage(thread),
425425
lastActivity: new Date(Number(thread.last_activity_at) / 1000),
426-
unread: Boolean(thread.has_newer),
426+
// This field is not documented but appears to indicate unread status
427+
unread: (thread as any).read_state === 1,
427428
}));
428429
} catch (error) {
429430
this.logger.error('Failed to fetch threads', error);

instagram-ts/source/ui/views/chat-view.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export default function ChatView() {
110110
if (!client) return;
111111

112112
const handleMessage = async (message: Message) => {
113+
// for current thread, append to message list and handle view changes
113114
if (message.threadId === chatState.currentThread?.id) {
114115
setChatState(prev => ({
115116
...prev,
@@ -135,7 +136,44 @@ export default function ChatView() {
135136

136137
// Mark item as seen
137138
await client.markItemAsSeen(chatState.currentThread.id, message.id);
139+
return;
138140
}
141+
142+
// if not current thread, update the global thread list to 1. show unread status 2. update last message preview
143+
setChatState(prev => {
144+
const threadIndex = prev.threads.findIndex(
145+
thread => thread.id === message.threadId,
146+
);
147+
if (threadIndex === -1) {
148+
return prev; // Thread not found, no update
149+
}
150+
151+
const updatedThreads = [...prev.threads];
152+
const threadToUpdate = updatedThreads[threadIndex]!;
153+
154+
// Update last message and unread status
155+
const updatedThread = {
156+
...threadToUpdate,
157+
lastActivity: message.timestamp,
158+
lastMessage: message,
159+
unread: true,
160+
};
161+
162+
updatedThreads[threadIndex] = updatedThread;
163+
164+
// Move the updated thread to the top of the list
165+
// This is more efficient than sorting the entire list
166+
updatedThreads.splice(threadIndex, 1);
167+
updatedThreads.unshift(updatedThread);
168+
169+
// Show notification for background message
170+
setSystemMessage('Someone else sent you a message!');
171+
172+
return {
173+
...prev,
174+
threads: updatedThreads,
175+
};
176+
});
139177
};
140178

141179
client.on('message', handleMessage);
@@ -225,6 +263,7 @@ export default function ChatView() {
225263
}
226264

227265
try {
266+
// polling always fetches messages in current thread
228267
const {messages: latestMessages} = await client.getMessages(
229268
chatState.currentThread.id,
230269
);
@@ -365,6 +404,8 @@ export default function ChatView() {
365404
const lastMessage = messages.at(-1);
366405

367406
if (lastMessage?.id) {
407+
// Mark as read in local and remote states
408+
thread.unread = false;
368409
await client.markThreadAsSeen(thread.id, lastMessage.id);
369410
}
370411
} catch (error) {

0 commit comments

Comments
 (0)