-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathtweets.js
3964 lines (3441 loc) · 133 KB
/
tweets.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* tweets.js
* Loads and displays tweets from a tweets.json file
*
* @author Caspar "UPLYNXED" Neervoort <twitter.com/upLYNXed>
* @version 1.0.0
* @license MIT
*
* @uses config.json - The file containing the config
* @uses tweets.json - The file containing the tweets to display
* @uses JSRender - The templating engine used to display the tweets
* @uses JSViews - The templating engine used to display the tweets
*
* Author: UPLYNXED (@uplynxed)
*/
/**
* The config object, this is used to store the config
* @type {object}
*/
let config = {
theme: "auto",
banner_pos_y: 65,
filters: {
"is_reply": "no_replies",
"is_retweet": "no_retweets",
"has_media": "all",
"is_favorite": "all",
},
};
/**
* The filters_config object, this is used to define the filters and their options.
* @type {object}
*
* @key {string} filter - The filter name
* @value {object} filter_def - The filter definition
* @value {string} filter_def.title - The title of the filter
* @value {string} filter_def.description - The description of the filter
* @value {string} filter_def.type - The type of the filter
* @value {string} filter_def.default - The default value of the filter
* @value {object} filter_def.options - The options of the filter
* @key {string} filter_def.options.option - The option name
* @value {string} filter_def.options.option.title - The title of the option
* @value {string} filter_def.options.option.description - The description of the option
* @value {string} filter_def.options.option.value - The value of the option
* @value {function} filter_def.options.option.callback - The callback function to call when the option is selected
*/
let filters_config = {
"is_reply": {
"title": "Replies",
"description": "Select whether to show tweets that are replies or not.",
"type": "select",
"default": "false",
"options": {
"no_replies": {
"title": "Hide replies",
"description": "Hide all replies",
"value": "false",
},
"all": {
"title": "Show replies",
"description": "Show all, including replies",
"value": "true",
},
"replies": {
"title": "Only show replies",
"description": "Only show replies",
"value": "only",
},
},
},
"is_retweet": {
"title": "Retweets",
"description": "Select whether to show tweets that are retweets or not.",
"type": "select",
"default": "false",
"options": {
"no_retweets": {
"title": "Hide retweets",
"description": "Hide all retweets",
"value": "false",
},
"retweets": {
"title": "Show retweets",
"description": "Show all, including retweets",
"value": "true",
},
"only_rt": {
"title": "Only show retweets",
"description": "Only show retweets (no quoted retweets)",
"value": "only",
},
"only_qrt": {
"title": "Only show quoted retweets",
"description": "Only show quoted retweets",
"value": "only_qrt",
},
"only_qrt_rt": {
"title": "Only show quoted retweets and retweets",
"description": "Only show quoted retweets and retweets",
"value": "only_qrt_rt",
}
},
},
"has_media": {
"title": "Media",
"description": "Select whether to show tweets that have media or not.",
"type": "select",
"default": "true",
"options": {
"false": {
"title": "Hide Media",
"description": "Hide all tweets with media",
"value": "false",
},
"true": {
"title": "Show Media",
"description": "Show all tweets with media",
"value": "true",
},
"only": {
"title": "Only Media",
"description": "Only show tweets with media",
"value": "only",
},
"only_img": {
"title": "Only Images",
"description": "Only show tweets with images",
"value": "only_img",
},
"only_vid": {
"title": "Only Videos",
"description": "Only show tweets with videos",
"value": "only_vid",
},
"only_gif": {
"title": "Only GIFs",
"description": "Only show tweets with GIFs",
"value": "only_gif",
},
"only_crd": {
"title": "Only Cards/Polls",
"description": "Only show tweets with cards or polls",
"value": "only_crd",
},
},
},
"is_favorited": {
"title": "Favorites",
"description": "Select whether to show tweets that are favorited or not.",
"type": "select",
"default": "true",
"options": {
"false": {
"title": "Hide favorited tweets",
"description": "Hide all favorited tweets",
"value": "false",
"callback": function() { },
},
"true": {
"title": "Show favorited tweets",
"description": "Show all favorited tweets",
"value": "true",
"callback": function() { },
},
"only": {
"title": "Only show favorited tweets",
"description": "Only show favorited tweets",
"value": "only",
"callback": function() { },
},
},
},
"date_cutoff": {
"title": "Date Cutoff",
"description": "Select whether to show tweets before or after a certain date.",
"type": "checkbox",
"default": false,
"options": {
"false": {
"title": "Show all tweets",
"description": "Show all tweets",
"value": false,
"callback": function() { },
},
"true": {
"title": "Show only tweets before a certain date",
"description": "Show only tweets before a certain date",
"value": true,
"callback": function() { },
},
},
},
};
/**
* Main user object, this is used to store the main user's data
* @type {object}
*/
let main_user = {};
/**
* The navigation hash object, this is used to store the hashes and their associated functions
* @type {object}
* @key {string} hash - The hash
* @value {function} func - The function to call when the hash is matched
*/
let navHash = {
"#": function() {
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Remove the hash from the URL
window.hash = "";
history.replaceState(null, null, window.location.pathname); // TODO: Figure this out
},
"#tweets": function( initial = false ) {
showLoadingAnimation();
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Set the "active" class on the "tweets" nav item only
let nav_items = document.querySelectorAll(".nav-item a");
nav_items.forEach(function(nav_item) {
nav_item.classList.remove("active");
});
document.querySelector(".nav-item#tweets a").classList.add("active");
// Set the title of the current loop
tweets_object['current_loop'].title = "Archived Tweets";
// Initialise with displayTweets() or switchTweetLoop()
if ( initial ) {
displayTweets({"tweets": tweets_object, "loop": "tweets_array", "offset": 0, "limit": 30});
} else {
switchTweetLoop("tweets_array", {"offset": 0, "limit": 30, "force": true});
}
},
"#media": function( initial = false ) {
showLoadingAnimation();
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Set the "active" class on the "media" nav item only
let nav_items = document.querySelectorAll(".nav-item a");
nav_items.forEach(function(nav_item) {
nav_item.classList.remove("active");
});
document.querySelector(".nav-item#media a").classList.add("active");
// Set the title of the current loop
tweets_object['current_loop'].title = "Media";
// Initialise with displayTweets() or switchTweetLoop()
if ( initial ) {
displayTweets({"tweets": tweets_object, "loop": "user_media", "offset": 0, "limit": 30});
} else {
switchTweetLoop("user_media", {"offset": 0, "limit": 30, "force": true});
}
},
"#favorites": function( initial = false ) {
showLoadingAnimation();
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Set the "active" class on the "favorites" nav item only
let nav_items = document.querySelectorAll(".nav-item a");
nav_items.forEach(function(nav_item) {
nav_item.classList.remove("active");
});
document.querySelector(".nav-item#favorites a").classList.add("active");
// Set the title of the current loop
tweets_object['current_loop'].title = "Favorited Tweets";
// Initialise with displayTweets() or switchTweetLoop()
if ( initial ) {
displayTweets({"tweets": tweets_object, "loop": "favorites", "offset": 0, "limit": 30});
} else {
switchTweetLoop("favorites", {"offset": 0, "limit": 30, "force": true});
}
},
"#all-in-archive": function( initial = false ) {
showLoadingAnimation();
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Set the "active" class on the "all-in-archive" nav item only
document.querySelector(".nav-item a.active").classList.remove("active");
tweets_object['all_in_archive'] = Object.values(tweets_object['tweets']).sort(function(a, b) {
if ( tweets_object['current_loop'].sort === "newest" ) {
return new Date(b.created_at) - new Date(a.created_at);
} else if ( tweets_object['current_loop'].sort === "oldest" ) {
return new Date(a.created_at) - new Date(b.created_at);
}
});
// Set the title of the current loop
tweets_object['current_loop'].title = "All Archived Tweets";
// Initialise with displayTweets() or switchTweetLoop()
if ( initial ) {
displayTweets({"tweets": tweets_object, "loop": "all_in_archive", "offset": 0, "limit": 30});
} else {
switchTweetLoop("all_in_archive", {"offset": 0, "limit": 30, "force": true});
}
},
"#search": function( initial = false ) { // TODO: Figure out how to implement this
showLoadingAnimation();
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Remove the "active" class from all nav items
let nav_items = document.querySelectorAll(".nav-item a");
nav_items.forEach(function(nav_item) {
nav_item.classList.remove("active");
});
// Set the title of the current loop
tweets_object['current_loop'].title = "Search";
// // Initialise with displayTweets() or switchTweetLoop()
// if ( initial ) {
// displayTweets({"tweets": tweets_object, "loop": "search_results"});
// } else {
// switchTweetLoop("search_results", {"force": true});
// }
},
"#replies": function() {
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// TODO: figure something out for this
},
"#retweets": function() {
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// Display the retweets
displayTweets({"tweets": tweets_object, "loop": "retweets"});
},
"#users": function() {
// TODO: figure something out for this
},
"#about": function() {
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: "instant" });
// TODO: figure something out for this, some kind of customizable about page
},
};
/**
* tweets_object - contains the tweets, users and other data
*
* @type {object}
* @property {object} tweets - The tweets loaded from the tweets.json file and processed (processTweets())
*
* @property {array} loaded_tweets - The current selection of tweets to display on the page
* @property {object} current_loop - Describes the context of the currently active loop of tweets
* @property {string} current_loop.name - The name of the current loop
* @property {string} current_loop.title - A title for the current loop to display on the page
* @property {array} current_loop.tweet_list - A working copy of the current loop of tweets that can be modified without affecting the original loop
* @property {integer} current_loop.offset - The offset of loaded tweets from the current loop
* @property {integer} current_loop.limit - The iterative limit of how many tweets to load at a time from the current loop
* @property {string} current_loop.sort - The sort order of the current loop
* @property {array} current_loop.users_relevant - The users relevant to the current loop, used for conversation views
*
* @property {object} users - The users loaded from the tweets.json file and processed
*
* @property {object} conversations - The conversations grouped by conversation ID
*
* @property {array} tweets_array - [loop] The tweets from the main user as an array (main user defined in config.json)
* @property {array} favorites - [loop] The favorited tweets
* @property {object} retweets - [loop] All retweets as an object
* @property {array} user_media - [loop] The media posted by the main user as an array
* @property {array} search_results - [loop] A dynamic array of search results, created as a search is performed
*
*
*/
let tweets_object = {
/**
* The tweets object, this contains the tweets as an object, with the tweet_id_str as the key so they can be accessed by ID
* @type {object}
* @key {string} id - The ID of the tweet
* @value {object} tweet - The tweet itself
*/
"tweets": {},
/**
* The loaded_tweets array, this contains the currently loaded tweets if there's any selected
* @type {Array}
* @value {object} tweet - The tweet itself
*/
"loaded_tweets": [],
/**
* The current_loops string, this identifies the current loop
* @type {string}
* @value {string} loop - The current loop
*/
"current_loop": {
"name": "tweets_array",
"title": "Archived Tweets", // TODO: Implement this
"tweet_list": () => {
delete tweets_object['current_loop'].tweet_list;
return tweets_object[tweets_object['current_loop'].name];
},
"offset": 0,
"limit": 30,
"sort": "newest",
"users_relevant": [],
},
/**
* The users object, this contains the users as an object, with the user_id_str as the key so they can be accessed by ID
* @type {object}
* @key {string} id - The ID of the user
* @value {object} user - The user itself
*/
"users": {},
/**
* The conversations object, this contains the replies as an object, with the conversation_id_str as the key so they can be accessed by the conversation ID
* @type {object}
* @key {string} conversation_id - The ID of the conversation
* @value {Array} replies - The replies themselves
* @value {object} replies.tweet - The tweet itself
*/
"conversations": {},
/**
* The tweets array, this contains the tweets as an array so they can be looped through
* @type {Array}
* @value {object} tweet - The tweet itself
*/
"tweets_array": [],
/**
* The favorites array, this contains the favorited tweets as an array so they can be looped through
* This value is stored locally in localStorage and can be exported and imported as a JSON file using exportFavorites() and importFavorites()
* @type {Array}
* @value {string} tweet_id_str - The ID of the tweet
*/
"favorites": [],
/**
* The retweets object, this contains the retweets as an array so they can be looped through
* @type {Array}
* @value {object} tweet - The tweet itself
*/
"retweets": [],
/**
* user_feed, this is a function that returns all the tweets and retweets by the main user as an array. It replaces itself with the array once it's been called once
* @returns {Array} - The tweets as an array
*/
"user_feed": () => {
// delete this.user_feed; // Delete the getter so it can be redefined
// this.user_feed = this.tweets_array.concat(this.retweets); // Redefine the getter
// tweets_object['tweets_array'].sort(function(a, b) { // Sort the array by date
// return new Date(a.created_at) - new Date(b.created_at);
// });
// return this.user_feed; // Return the array
},
/**
* user_media, this is a function that returns all the media posted by the main user as an array. It replaces itself with the array once it's been called once
* @returns {Array} - The media as an array
*/
get user_media() {
delete this.user_media; // Delete the getter so it can be redefined
// Do not continue if this is not in the top level of the tweets_object
if ( this.tweets_array === undefined ) { // Does not work because the tweets_array is referenced in the prototype in the __proto__ object
return undefined;
}
this.user_media = this.tweets_array.filter(function(tweet) { // Redefine user_media
return tweet.extended_entities !== undefined;
});
return this.user_media; // Return the array
},
__proto__: { // The prototype of the tweets object
/**
* Get tweet poster's user object
* @returns {object} - The user object
*/
get user() {
delete this.user; // Delete the getter so it can be redefined
if ( this.user_id_str === undefined ) { // If there is no user associated with the given object
return undefined;
} else if ( tweets_object.users[this.user_id_str] === undefined ) { // If the user doesn't exist in the users object
return undefined;
}
return tweets_object.users[this.user_id_str];
},
/**
* Get a tweet's conversation by ID
* @returns {Object} - The conversation object
* @returns {Array} - The conversation object.tweets - The tweets in the conversation
* @returns {Integer} - The conversation object.index - The index of the current tweet in the conversation array
*/
get conversation() {
delete this.conversation; // Delete the getter so it can be redefined
if ( this.conversation_id_str === undefined || this.user_id_str === undefined ) { // If there is no conversation associated with the given object or no user associated with the given object
return undefined;
}
if ( tweets_object['conversations'][this.conversation_id_str] === undefined ) { // If the conversation doesn't exist in the replies object
return undefined;
}
let conversation = {
"tweets": tweets_object['conversations'][this.conversation_id_str],
"index": 0,
};
// Get the index of the current tweet in the conversation array
conversation.index = conversation.tweets.indexOf(this);
return conversation;
},
/**
* Get a tweet's replies
* @returns {Array} - The replies
*/
get replies() {
delete this.replies; // Delete the getter so it can be redefined
if ( this.user_id_str === undefined ) { // If there is no user associated with the given object
return undefined;
}
let replies = [];
// If the tweet's id_str exists as a key in the conversations object, return that property
if ( tweets_object['conversations'][this.id_str] !== undefined ) {
replies = tweets_object['conversations'][this.id_str];
// Remove the current tweet from the replies array
replies = replies.filter(function(tweet) {
return tweet.id_str !== this.id_str;
}, this);
this.replies = replies; // Redefine the getter
return replies; // Return the replies as we don't need to look further
}
// If the tweet's conversation_id_str exists as a key in the conversations object, look for the tweet in the conversation
if ( this.conversation_id_str !== undefined && tweets_object['conversations'][this.conversation_id_str] !== undefined ) {
// Get any tweet with it's in_reply_to_status_id_str set to the current tweet's id_str
replies = tweets_object['conversations'][this.conversation_id_str].filter(function(tweet) {
return tweet.in_reply_to_status_id_str === this.id_str;
}, this);
} else {
// Get any tweet with it's in_reply_to_status_id_str set to the current tweet's id_str in a parent conversation object
replies = Object.values(tweets_object['tweets']).filter(function(tweet) {
return tweet.in_reply_to_status_id_str === this.id_str;
}, this);
}
this.replies = replies; // Redefine the getter
return replies;
},
/**
* Get tweet's or a user's url
* @returns {string} - The url
*/
get url_path() {
delete this.url_path; // Delete the getter so it can be redefined
let url_path = "";
if ( this.screen_name !== undefined ) { // If the given object is a user
if ( this.id_str === main_user.id_str ) { // If the given user is the main user
url_path = "#";
return "#";
}
url_path = `https://twitter.com/${this.screen_name}`;
this.url_path = url_path;
return url_path;
} else if ( this.user_id_str !== undefined ) { // If the given object is a tweet or has an associated user
if ( this.user_id_str === main_user.id_str ) { // If the given tweet is by the main user
url_path = "#";
} else { // If the given tweet is not by the main user
url_path = "https://twitter.com/"
}
url_path += `${this.user.screen_name}/status/${this.id_str}`; // TODO: Check if tweet is in archive and use local URL if it is
this.url_path = url_path;
return url_path;
} else if ( this.id_str === undefined ) { // If the given object is not a tweet or a user somehow
url_path = 'javascript:console.log("Error: Not a valid object.");';
this.url_path = url_path;
return url_path;
} else { // If it is a valid tweet or user, but something else went wrong
url_path = 'javascript:console.log("Error: Something went wrong.");';
this.url_path = url_path;
return url_path;
}
},
}
};
/**
* The media replacements object, this is used to keep track of media URL replacements to local or archived URLs
* @type {object}
* @key {string} url - The URL of the media
* @property {string} filename - The filename of the media
* @property {string} orig_url - The original URL of the media
* @property {string} type - The type of media
* @property {string} url - The local URL of the media in case it was downloaded
* @property {string} wayback_url - The Wayback Machine URL of the media in case it has been archived there
* @property {string} resolved_url - The URL that the media was successfully loaded from on the page
*/
let media_replacements = {};
/**
* Initialize the page
*
* @returns {void}
*/
function init() {
// Attach the scroll handler
attachScrollHandler();
// Load the config
config = loadConfig();
// Load the tweets
loadTweets("tweets.json");
// Load the media replacements from localStorage
loadMediaReplacements();
// Set the theme
setTheme(config.theme);
// Configure the templating engine (JSRender)
$.views.settings.allowCode(true); // Allow code in tags
registerCustomTags(); // Register custom tags
// Set the main user
setMainUser(tweets_object['users'][config.id]);
// Process the users
processUsers();
// Process the tweets and display them
processTweets(() => {
loadFavorites();
handleNavHashChange();
});
// // Run navigation hash handler on page load
// handleNavHashChange();
// Attach the breakpoint resize handlers
attachBreakpointHandlers();
// Attach the navigation hash handler
attachNavHashHandler();
}
/**
* Loads the config from a config.json file
*
* @returns {object} - The config object
*/
function loadConfig(file = "config.json") {
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) { // If the request is successful
config_file = JSON.parse(this.responseText); // Parse the response as JSON;
// Merge the config_file object with the config object, overwriting any existing values
Object.assign(config, config_file);
// Copy the date_cutoff related properties to the tweets_object.current_loop object
try {
config.date_cutoff = new Date(config.date_cutoff);
tweets_object['current_loop'].date_cutoff = config.date_cutoff.getTime();
if (config.date_cutoff_toggle !== undefined && config.date_cutoff != false) {
tweets_object['current_loop'].date_cutoff_toggle = (config.date_cutoff_toggle === undefined) ? false : config.date_cutoff_toggle;
tweets_object['current_loop'].date_cutoff_toggle_option = (config.date_cutoff_toggle_option === undefined) ? true : config.date_cutoff_toggle_option;
} else {
tweets_object['current_loop'].date_cutoff_toggle = false;
tweets_object['current_loop'].date_cutoff_toggle_option = false;
}
} catch (e) {
console.error( 'Error: Invalid date_cutoff value in config.json', e );
}
}
};
// Build the request URL
let request_url = "";
if (window.location.pathname.endsWith("/") || window.location.pathname == "/") {
request_url = window.location.href.split("#");
request_url = request_url[0] + file;
} else {
request_url = window.location.href.split("#");
request_url = request_url[0].split("/");
request_url.pop();
request_url = request_url.join("/") + "/" + file;
}
xhttp.open("GET", request_url, false);
xhttp.send();
return config;
}
/**
* Loads the tweets from a tweets.json file
*
* @param {string} file - The file to load tweets from
* @param {function} callback - The callback function to call when the tweets are loaded
*
* @returns {object} - The tweets object
*/
function loadTweets(file = "tweets.json", callback = function() {}) {
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) { // If the request is successful
// Merge the tweets_object with the JSON file object, overwriting any existing values
Object.assign(tweets_object, JSON.parse(this.responseText));
Object.entries(tweets_object['tweets']).forEach(function([key, value]) {
// Optimize the tweet
tweets_object['tweets'][key] = optimizeTweet(tweets_object['tweets'][key]);
// Initialize the tweet object
tweets_object['tweets'][key] = initializeTweet(tweets_object['tweets'][key]);
});
}
callback;
};
// Build the request URL
let request_url = "";
if (window.location.pathname.endsWith("/") || window.location.pathname == "/") {
request_url = window.location.href.split("#");
request_url = request_url[0] + file;
} else {
request_url = window.location.href.split("#");
request_url = request_url[0].split("/");
request_url.pop();
request_url = request_url.join("/") + "/" + file;
}
xhttp.open("GET", request_url, false);
xhttp.send();
return tweets_object;
}
/**
* Initialize a tweet object
* - Sets the prototype of the tweet to the tweets_object prototype
* - Converts the date to a Date object
*
* @param {object} tweet - The tweet object to initialize
*
* @returns {object} - The initialized tweet object
*/
function initializeTweet(tweet) {
// Set the prototype of the tweet to the tweets_object prototype
Object.setPrototypeOf( tweet, tweets_object.__proto__ );
// If tweet contains a retweet, set the prototype of the retweet to the tweets_object prototype
if ( tweet.retweeted_status_result !== undefined ) {
let retweet = tweet.retweeted_status_result.result;
// Add the retweeted user to the users object if it doesn't exist
if (tweets_object['users'][retweet.core.user_results.result.rest_id] !== undefined) {
tweets_object['users'][retweet.core.user_results.result.rest_id] = retweet.core.user_results.result.legacy;
tweets_object['users'][retweet.core.user_results.result.rest_id].id_str = retweet.core.user_results.result.rest_id;
}
// If the retweet contains a quoted tweet, set the prototype of the quoted tweet to the tweets_object prototype
if ( retweet.quoted_status_result !== undefined ) {
if ( retweet.quoted_status_result.result.legacy !== undefined ) {
let quoted_tweet = tweet.retweeted_status_result.result.quoted_status_result.result;
// Change the date format of the tweets
quoted_tweet.legacy.created_at = new Date(tweet.created_at).getTime();
// Add the quoted tweet to the tweets_object.tweets object
tweets_object['tweets'][quoted_tweet.legacy.id_str] = quoted_tweet.legacy;
// Add the quoted tweet user to the users object if it doesn't exist
if (tweets_object['users'][quoted_tweet.core.user_results.result.rest_id] !== undefined) {
tweets_object['users'][quoted_tweet.core.user_results.result.rest_id] = quoted_tweet.core.user_results.result.legacy;
tweets_object['users'][quoted_tweet.core.user_results.result.rest_id].id_str = quoted_tweet.core.user_results.result.rest_id;
}
Object.setPrototypeOf( quoted_tweet.legacy, tweets_object.__proto__ );
}
}
Object.setPrototypeOf( retweet.legacy, tweets_object.__proto__ );
}
// Change the date format of the tweets
tweet.created_at = new Date(tweet.created_at).getTime();
return tweet;
}
/**
* Optimize a tweet object
*
* @param {object} tweet - The tweet object to optimize
*
* @returns {object} - The optimized tweet object
*/
function optimizeTweet(tweet) {
// Remove unused properties
let unused_properties = [
"bookmarked",
"coordinates",
"display_text_range",
"ext",
"ext_edit_control",
"ext_views",
"geo",
"id",
"in_reply_to_status_id",
"in_reply_to_user_id",
"lang",
"place",
"possibly_sensitive",
"possibly_sensitive_editable",
"supplemental_language",
"truncated",
"user_id",
];
unused_properties.forEach(function(property) {
delete tweet[property];
});
// Remove unused properties from the media objects
if (tweet.extended_entities !== undefined && tweet.entities !== undefined) {
let unused_media_properties = [
"additional_media_info",
"ext_sensitive_media_warning",
"features",
"id",
"indices",
];
unused_media_properties.forEach(function(property) {
if (tweet.extended_entities.media !== undefined) {
tweet.extended_entities.media.forEach(function(media) {
delete media[property];
});
}
if (tweet.entities.media !== undefined) {
tweet.entities.media.forEach(function(media) {
delete media[property];
});
}
});
}
return tweet;
}
/**
* Process the tweets object
*
* @param {function} callback - The callback function to call when the tweets are processed
*
* @returns {object} - The processed tweets object
*/
function processTweets(callback = function() {}) {
// Convert tweets to an array
tweets_object['tweets_array'] = Object
.keys(tweets_object['tweets'])
.map(function(key) {
return tweets_object['tweets'][key];
});
console.log(tweets_object);
// Remove ads from the tweets
tweets_object['tweets_array'] = discardAds(tweets_object['tweets_array']);
// Handle Quoted Tweets and Replies
let tweets_quoted = []; // An array to store the quoted tweets
tweets_object['tweets_array'].forEach(function(tweet) {
/** Move any quoted tweets to a "quoted_tweet" property inside the tweet that quoted it and:
* - merge the quoted tweet's "entities.user_mentions" with the main tweet's "entities.user_mentions"
* - merge the quoted tweet's "entities.urls" with the main tweet's "entities.urls"
* - add the quoted tweet to the tweets_quoted array so we can filter it out later
*/
if (tweet.quoted_status_id_str !== undefined) {
let quoted_tweet = tweets_object['tweets'][tweet.quoted_status_id_str]; // Get the quoted tweet
tweet.quoted_tweet = quoted_tweet; // Add the quoted tweet to the tweet object
if (tweet.quoted_tweet !== undefined) { // If the quoted tweet exists
tweet.quoted_tweet.entities.user_mentions.forEach(function(user_mention) { // Merge the quoted tweet's user mentions with the main tweet's user mentions
tweet.entities.user_mentions.push(user_mention);
});
tweet.quoted_tweet.entities.urls.forEach(function(url) { // Merge the quoted tweet's urls with the main tweet's urls
tweet.entities.urls.push(url);
});
}
tweets_quoted.push(quoted_tweet);
}
// Copy any conversation tweets to a "conversations" object inside the tweets_object
if (tweet.conversation_id_str !== undefined) {
if (tweets_object['conversations'][tweet.conversation_id_str] === undefined) { // If the conversation doesn't exist in the replies object
tweets_object['conversations'][tweet.conversation_id_str] = []; // Create the conversation in the replies object
}
tweets_object['conversations'][tweet.conversation_id_str].push(tweet);
}
// Copy any tweets that are replies to a "replies" object inside the tweet object it is replying to
if (tweet.in_reply_to_status_id_str !== undefined && tweet.in_reply_to_status_id_str !== null) {
let tweet_replied_to = tweets_object['tweets'][tweet.in_reply_to_status_id_str]; // Get the tweet this tweet is replying to
if (tweet_replied_to !== undefined) { // If the tweet this tweet is replying to exists
if (tweet_replied_to.replies === undefined) { // If the tweet this tweet is replying to doesn't have a replies object
tweet_replied_to.replies = []; // Create the replies object
}
tweet_replied_to.replies.push( tweet ); // Add the tweet to the replies object
}
}
// Modify retweets
/** If the tweet is a retweet:
* - rename the tweet's "user" property to the "retweeting_user" property
* - rename the tweet's "user_id_str" to "retweeting_user_id_str"
* - replace the retweet's "user" to the main tweet "user"
* - replace the tweet's "full_text" with the retweet's "full_text"
* - replace the retweet's "entities" to the main tweet's "entities"
* - replace the retweet's "extended_entities" to the main tweet's "extended_entities"
* - replace the retweet's "is_quote_status" to the main tweet's "is_quote_status"
* - replace the retweet's "quoted_status_id_str" to the main tweet's "quoted_status_id_str"
* - replace the retweet's "quoted_status_permalink" to the main tweet's "quoted_status_permalink"
* - replace the retweet's "reply_count", "retweet_count", and "favorite_count" to the main tweet
*/
if (tweet.retweeted_status_result !== undefined) {
let retweet = tweet.retweeted_status_result.result.legacy;
let retweet_user = tweet.retweeted_status_result.result.core.user_results.result.legacy;
retweet_user['id_str'] = tweet.retweeted_status_result.result.core.user_results.result.rest_id;
tweet.retweeting_user = tweet.user;
tweet.retweeting_user_id_str = tweet.user_id_str;
tweet.user = retweet_user;
tweet.user_id_str = retweet_user.id_str;
tweet.full_text = retweet.full_text;
tweet.entities = retweet.entities;
tweet.extended_entities = retweet.extended_entities;
tweet.is_quote_status = retweet.is_quote_status;
tweet.quoted_status_id_str = retweet.quoted_status_id_str;
tweet.quoted_status_permalink = retweet.quoted_status_permalink;
tweet.reply_count = retweet.reply_count;
tweet.retweet_count = retweet.retweet_count;
tweet.favorite_count = retweet.favorite_count;
// If a tweet contains a "quoted_status_result" property, copy it to the tweet's "quoted_tweet" property
if (tweet.retweeted_status_result.result.quoted_status_result !== undefined) {
// Check for a tombstone property
if (tweet.retweeted_status_result.result.quoted_status_result.result.tombstone !== undefined) {
tweet.is_quote_status = true;
} else {
tweet.quoted_tweet = tweet.retweeted_status_result.result.quoted_status_result.result.legacy;
}
}
}
// Add filter object properties to the tweet object
tweet['filters'] = {
'is_by_main_user': tweet.user_id_str === main_user.id_str,
'is_reply': tweet.in_reply_to_status_id_str !== undefined && tweet.in_reply_to_status_id_str !== null,
'is_reply_to_main_user': tweet.in_reply_to_user_id_str === main_user.id_str,
'is_retweet': tweet.retweeted_status_result !== undefined,
'is_quote': tweet.is_quote_status === true,
'has_media': tweet.extended_entities !== undefined || tweet.entities.media !== undefined || (tweet.entities.urls !== undefined && tweet.entities.urls.length > 0) || tweet.card !== undefined,
'media_types': {},
'has_mention': tweet.entities?.user_mentions !== undefined && tweet.entities?.user_mentions?.length > 0,
'has_hashtag': tweet.entities?.hashtags !== undefined && tweet.entities?.hashtags?.length > 0,
};
tweet.filters['media_types'] = (() => {
delete tweet.filters.media_types; // Delete the getter so it can be redefined
let media_types = {};
if (tweet.filters.has_media === true) {
if (tweet.extended_entities?.media !== undefined) {
tweet.extended_entities.media.forEach(function(media) {
media_types[media.type] = true;
});
}
if (tweet.card !== undefined) {
if (tweet.card.name.includes("poll")) {
media_types['poll'] = true;