mirror of
https://github.com/feeddeck/feeddeck.git
synced 2026-03-09 07:02:01 -05:00
[x] Remove X Source (#162)
X (Twitter) was never supported and the code was never used to add a source. We kept it in the hope that it will be possible again to use X within the app. Since this isn't the case it is time to remove the code.
This commit is contained in:
@@ -21,7 +21,6 @@ import 'package:feeddeck/utils/fd_icons.dart';
|
||||
/// - [rss]
|
||||
/// - [stackoverflow]
|
||||
/// - [tumblr]
|
||||
/// - [x]
|
||||
/// - [youtube]
|
||||
///
|
||||
/// The [none] value is not valid and just here as a fallback in case sth. odd
|
||||
@@ -42,7 +41,6 @@ enum FDSourceType {
|
||||
rss,
|
||||
stackoverflow,
|
||||
tumblr,
|
||||
// x,
|
||||
youtube,
|
||||
none,
|
||||
}
|
||||
@@ -85,8 +83,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return 'StackOverflow';
|
||||
case FDSourceType.tumblr:
|
||||
return 'Tumblr';
|
||||
// case FDSourceType.x:
|
||||
// return 'X';
|
||||
case FDSourceType.youtube:
|
||||
return 'YouTube';
|
||||
default:
|
||||
@@ -123,8 +119,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return FDIcons.stackoverflow;
|
||||
case FDSourceType.tumblr:
|
||||
return FDIcons.tumblr;
|
||||
// case FDSourceType.x:
|
||||
// return FDIcons.x;
|
||||
case FDSourceType.youtube:
|
||||
return FDIcons.youtube;
|
||||
default:
|
||||
@@ -161,8 +155,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return const Color(0xffef8236);
|
||||
case FDSourceType.tumblr:
|
||||
return const Color(0xff34526f);
|
||||
// case FDSourceType.x:
|
||||
// return const Color(0xff000000);
|
||||
case FDSourceType.youtube:
|
||||
return const Color(0xffff0000);
|
||||
default:
|
||||
@@ -200,8 +192,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.tumblr:
|
||||
return const Color(0xffffffff);
|
||||
// case FDSourceType.x:
|
||||
// return const Color(0xffffffff);
|
||||
case FDSourceType.youtube:
|
||||
return const Color(0xffffffff);
|
||||
default:
|
||||
@@ -291,7 +281,6 @@ class FDSourceOptions {
|
||||
String? rss;
|
||||
FDStackOverflowOptions? stackoverflow;
|
||||
String? tumblr;
|
||||
String? x;
|
||||
String? youtube;
|
||||
|
||||
FDSourceOptions({
|
||||
@@ -308,7 +297,6 @@ class FDSourceOptions {
|
||||
this.rss,
|
||||
this.stackoverflow,
|
||||
this.tumblr,
|
||||
this.x,
|
||||
this.youtube,
|
||||
});
|
||||
|
||||
@@ -364,9 +352,6 @@ class FDSourceOptions {
|
||||
responseData.containsKey('tumblr') && responseData['tumblr'] != null
|
||||
? responseData['tumblr']
|
||||
: null,
|
||||
x: responseData.containsKey('x') && responseData['x'] != null
|
||||
? responseData['x']
|
||||
: null,
|
||||
youtube:
|
||||
responseData.containsKey('youtube') && responseData['youtube'] != null
|
||||
? responseData['youtube']
|
||||
@@ -389,7 +374,6 @@ class FDSourceOptions {
|
||||
'rss': rss,
|
||||
'stackoverflow': stackoverflow?.toJson(),
|
||||
'tumblr': tumblr,
|
||||
'x': x,
|
||||
'youtube': youtube,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,11 +129,6 @@ class ItemDetails extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
// case FDSourceType.x:
|
||||
// return ItemDetailsX(
|
||||
// item: item,
|
||||
// source: source,
|
||||
// );
|
||||
case FDSourceType.youtube:
|
||||
return ItemDetailsYoutube(
|
||||
item: item,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media_gallery.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
|
||||
class ItemDetailsX extends StatelessWidget {
|
||||
const ItemDetailsX({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingExtraSmall,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -110,11 +110,6 @@ class ItemPreview extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
// case FDSourceType.x:
|
||||
// return ItemPreviewX(
|
||||
// item: item,
|
||||
// source: source,
|
||||
// );
|
||||
case FDSourceType.youtube:
|
||||
return ItemPreviewYoutube(
|
||||
item: item,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/widgets/item/preview/utils/details.dart';
|
||||
import 'package:feeddeck/widgets/item/preview/utils/item_actions.dart';
|
||||
import 'package:feeddeck/widgets/item/preview/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/preview/utils/item_media_gallery.dart';
|
||||
import 'package:feeddeck/widgets/item/preview/utils/item_source.dart';
|
||||
|
||||
class ItemPreviewX extends StatelessWidget {
|
||||
const ItemPreviewX({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ItemActions(
|
||||
item: item,
|
||||
onTap: () => showDetails(context, item, source),
|
||||
children: [
|
||||
ItemSource(
|
||||
sourceTitle: item.author ?? '',
|
||||
sourceSubtitle: '${source.type.toLocalizedString()}: ${source.title}',
|
||||
sourceType: source.type,
|
||||
sourceIcon: source.icon,
|
||||
itemPublishedAt: item.publishedAt,
|
||||
itemIsRead: item.isRead,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -98,10 +98,6 @@ class _AddSourceState extends State<AddSource> {
|
||||
return AddSourceTumblr(column: widget.column);
|
||||
}
|
||||
|
||||
// if (_sourceType == FDSourceType.x) {
|
||||
// return AddSourceX(column: widget.column);
|
||||
// }
|
||||
|
||||
if (_sourceType == FDSourceType.youtube) {
|
||||
return AddSourceYouTube(column: widget.column);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ The Lemmy source can be used to follow your favorite Lemmy communities.
|
||||
of this instance (e.g. `https://lemmy.world`).
|
||||
''';
|
||||
|
||||
/// The [AddSourceLemmy] widget is used to display the form to add a new Reddit
|
||||
/// The [AddSourceLemmy] widget is used to display the form to add a new Lemmy
|
||||
/// source.
|
||||
class AddSourceLemmy extends StatefulWidget {
|
||||
const AddSourceLemmy({
|
||||
@@ -42,8 +42,8 @@ class _AddSourceLemmyState extends State<AddSourceLemmy> {
|
||||
bool _isLoading = false;
|
||||
String _error = '';
|
||||
|
||||
/// [_addSource] adds a new Reddit source. The user can provide a subreddit or
|
||||
/// a user. It is also possible to provide the complete RSS feed url.
|
||||
/// [_addSource] adds a new Lemmy source. The user can provide a Lemmy url,
|
||||
/// which could be be a community or user or the corresponding RSS feed.
|
||||
Future<void> _addSource() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
|
||||
import 'package:feeddeck/models/column.dart';
|
||||
import 'package:feeddeck/utils/api_exception.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/utils/openurl.dart';
|
||||
import 'package:feeddeck/widgets/source/add/add_source_form.dart';
|
||||
|
||||
const _helpText = '''
|
||||
The X (formerly Twitter) source can be used to follow a user on X, e.g. `@rico_berger`.
|
||||
''';
|
||||
|
||||
/// The [AddSourceX] widget is used to display the form to add a new X
|
||||
/// (formerly) source.
|
||||
class AddSourceX extends StatefulWidget {
|
||||
const AddSourceX({
|
||||
super.key,
|
||||
required this.column,
|
||||
});
|
||||
|
||||
final FDColumn column;
|
||||
|
||||
@override
|
||||
State<AddSourceX> createState() => _AddSourceXState();
|
||||
}
|
||||
|
||||
class _AddSourceXState extends State<AddSourceX> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _xController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
String _error = '';
|
||||
|
||||
/// [_addSource] adds a new X source where the user can provide the username
|
||||
/// of a x user.
|
||||
Future<void> _addSource() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = '';
|
||||
});
|
||||
|
||||
try {
|
||||
// AppRepository app = Provider.of<AppRepository>(context, listen: false);
|
||||
// await app.addSource(
|
||||
// widget.column.id,
|
||||
// FDSourceType.x,
|
||||
// FDSourceOptions(
|
||||
// x: _xController.text,
|
||||
// ),
|
||||
// );
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_error = '';
|
||||
});
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} on ApiException catch (err) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_error = 'Failed to add source: ${err.message}';
|
||||
});
|
||||
} catch (err) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_error = 'Failed to add source: ${err.toString()}';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_xController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AddSourceForm(
|
||||
onTap: _addSource,
|
||||
isLoading: _isLoading,
|
||||
error: _error,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownBody(
|
||||
selectable: true,
|
||||
data: _helpText,
|
||||
onTapLink: (text, href, title) {
|
||||
try {
|
||||
if (href != null) {
|
||||
openUrl(href);
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
),
|
||||
TextFormField(
|
||||
controller: _xController,
|
||||
keyboardType: TextInputType.text,
|
||||
autocorrect: false,
|
||||
enableSuggestions: true,
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Username',
|
||||
),
|
||||
onFieldSubmitted: (value) => _addSource(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ export default function Home() {
|
||||
<RSS />
|
||||
<StackOverflow />
|
||||
<Tumblr />
|
||||
<X />
|
||||
<YouTube />
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,24 +422,6 @@ const Tumblr = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const X = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
width="32px"
|
||||
height="32px"
|
||||
viewBox="0 0 4096 4096"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g id="X" transform="matrix(8.90048,0,0,8.90048,-238.533,-230.522)">
|
||||
<path d="M389.2,48L459.8,48L305.6,224.2L487,464L345,464L233.7,318.6L106.5,464L35.8,464L200.7,275.5L26.8,48L172.4,48L272.9,180.9L389.2,48ZM364.4,421.8L403.5,421.8L151.1,88L109.1,88L364.4,421.8Z" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="pt-4">X</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const YouTube = () => (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg
|
||||
|
||||
@@ -19,7 +19,6 @@ import { IProfile } from '../models/profile.ts';
|
||||
import { getNitterFeed } from './nitter.ts';
|
||||
import { getMastodonFeed } from './mastodon.ts';
|
||||
import { getFourChanFeed, isFourChanUrl } from './fourchan.ts';
|
||||
// import { getXFeed } from './x.ts';
|
||||
|
||||
/**
|
||||
* `getFeed` returns a feed which consist of a source and a list of items for
|
||||
@@ -194,8 +193,6 @@ export const getFeed = async (
|
||||
source,
|
||||
feedData,
|
||||
);
|
||||
// case 'x':
|
||||
// return await getXFeed(supabaseClient, redisClient, profile, source, data);
|
||||
case 'youtube':
|
||||
return await getYoutubeFeed(
|
||||
supabaseClient,
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Redis } from 'redis';
|
||||
import { unescape } from 'lodash';
|
||||
|
||||
import { IItem } from '../models/item.ts';
|
||||
import { ISource } from '../models/source.ts';
|
||||
import { feedutils } from './utils/index.ts';
|
||||
import { IProfile } from '../models/profile.ts';
|
||||
import { utils } from '../utils/index.ts';
|
||||
|
||||
export const getXFeed = async (
|
||||
supabaseClient: SupabaseClient,
|
||||
_redisClient: Redis | undefined,
|
||||
_profile: IProfile,
|
||||
source: ISource,
|
||||
_feedData: string | undefined,
|
||||
): Promise<{ source: ISource; items: IItem[] }> => {
|
||||
if (!source.options?.x || source.options.x.length === 0) {
|
||||
throw new feedutils.FeedValidationError('Invalid source options');
|
||||
}
|
||||
|
||||
if (source.options.x[0] !== '@') {
|
||||
throw new feedutils.FeedValidationError('Invalid source options');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed for the provided X username, based on the content of the HTML
|
||||
* returned for the syndication.twitter.com url.
|
||||
*/
|
||||
const response = await utils.fetchWithTimeout(
|
||||
generateFeedUrl(source.options.x),
|
||||
{ method: 'get' },
|
||||
5000,
|
||||
);
|
||||
const html = await response.text();
|
||||
|
||||
const matches = html.match(
|
||||
/script id="__NEXT_DATA__" type="application\/json">([^>]*)<\/script>/,
|
||||
);
|
||||
if (!matches || matches.length !== 2) {
|
||||
throw new Error('Invalid feed');
|
||||
}
|
||||
const feed = JSON.parse(matches[1]) as Feed;
|
||||
|
||||
/**
|
||||
* Generate a source id based on the user id, column id and the normalized
|
||||
* `twitter` options. Besides that we also set the source type to `twitter`
|
||||
* and the link for the source. In opposite to the other sources we do not use
|
||||
* the title of the feed as the title for the source, instead we are using the
|
||||
* user input as title.
|
||||
*/
|
||||
if (source.id === '') {
|
||||
source.id = await generateSourceId(
|
||||
source.userId,
|
||||
source.columnId,
|
||||
source.options.x,
|
||||
);
|
||||
}
|
||||
source.type = 'x';
|
||||
source.title = source.options.x;
|
||||
source.link = `https://twitter.com/${source.options.x.slice(1)}`;
|
||||
|
||||
/**
|
||||
* When the source doesn't has an icon yet and the user requested the feed of
|
||||
* a user (string starts with `@`) we try to get an icon for the source from
|
||||
* the first item in the returned feed.
|
||||
*/
|
||||
if (
|
||||
!source.icon && source.options.x[0] === '@' &&
|
||||
feed.props.pageProps.timeline.entries.length > 0 &&
|
||||
feed.props.pageProps.timeline.entries[0].content.tweet.user
|
||||
.profile_image_url_https
|
||||
) {
|
||||
source.icon = feed.props.pageProps.timeline.entries[0].content.tweet.user
|
||||
.profile_image_url_https;
|
||||
source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that the source does contain all the required information we can start
|
||||
* to generate the items for the source, by looping over all the feed entries.
|
||||
* We only add the first 50 items from the feed, because we only keep the
|
||||
* latest 50 items for each source in our deletion logic.
|
||||
*/
|
||||
const items: IItem[] = [];
|
||||
|
||||
for (
|
||||
const [index, entry] of feed.props.pageProps.timeline.entries.entries()
|
||||
) {
|
||||
if (index === 50) {
|
||||
break;
|
||||
}
|
||||
|
||||
const media = getMedia(entry);
|
||||
|
||||
items.push({
|
||||
id: await generateItemId(source.id, entry.content.tweet.id_str),
|
||||
userId: source.userId,
|
||||
columnId: source.columnId,
|
||||
sourceId: source.id,
|
||||
title: '',
|
||||
link: `https://twitter.com${entry.content.tweet.permalink}`,
|
||||
description: unescape(entry.content.tweet.full_text),
|
||||
author: entry.content.tweet.user.screen_name,
|
||||
options: media && media.length > 0 ? { media: media } : undefined,
|
||||
publishedAt: Math.floor(
|
||||
new Date(entry.content.tweet.created_at).getTime() / 1000,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return { source, items };
|
||||
};
|
||||
|
||||
/**
|
||||
* `generateFeedUrl` returns the url to get the Tweets of the provided username.
|
||||
* Since we check before that the input must start with an `@` we do not need to
|
||||
* check if the input is a valid username.
|
||||
*/
|
||||
const generateFeedUrl = (input: string): string => {
|
||||
return `https://syndication.twitter.com/srv/timeline-profile/screen-name/${
|
||||
input.slice(1)
|
||||
}?showReplies=true`;
|
||||
};
|
||||
|
||||
/**
|
||||
* `generateSourceId` generates a unique source id based on the user id, column
|
||||
* id and the link of the RSS feed. We use the MD5 algorithm for the link to
|
||||
* generate the id.
|
||||
*/
|
||||
const generateSourceId = async (
|
||||
userId: string,
|
||||
columnId: string,
|
||||
link: string,
|
||||
): Promise<string> => {
|
||||
return `x-${userId}-${columnId}-${await utils.md5(link)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* `generateItemId` generates a unique item id based on the source id and the
|
||||
* identifier of the item. We use the MD5 algorithm for the identifier, which
|
||||
* can be the link of the item or the id of the item.
|
||||
*/
|
||||
const generateItemId = async (
|
||||
sourceId: string,
|
||||
identifier: string,
|
||||
): Promise<string> => {
|
||||
return `${sourceId}-${await utils.md5(identifier)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* `getMedia` returns an image for the provided feed entry. To get the images we
|
||||
* have to check the `entities.media`, `retweeted_status.media` and
|
||||
* `extended_entities.media` properties of the feed entry.
|
||||
*/
|
||||
const getMedia = (entry: Entry): string[] | undefined => {
|
||||
if (
|
||||
entry.content.tweet.retweeted_status?.extended_entities?.media &&
|
||||
entry.content.tweet.retweeted_status.extended_entities.media.length > 0
|
||||
) {
|
||||
const images = [];
|
||||
|
||||
for (
|
||||
const media of entry.content.tweet.retweeted_status.extended_entities
|
||||
.media
|
||||
) {
|
||||
if (media.type === 'photo') {
|
||||
images.push(media.media_url_https);
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
if (
|
||||
entry.content.tweet.retweeted_status?.entities?.media &&
|
||||
entry.content.tweet.retweeted_status?.entities.media.length > 0
|
||||
) {
|
||||
const images = [];
|
||||
|
||||
for (const media of entry.content.tweet.retweeted_status.entities.media) {
|
||||
if (media.type === 'photo') {
|
||||
images.push(media.media_url_https);
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
if (
|
||||
entry.content.tweet.retweeted_status?.media &&
|
||||
entry.content.tweet.retweeted_status.media.length > 0
|
||||
) {
|
||||
const images = [];
|
||||
|
||||
for (const media of entry.content.tweet.retweeted_status.media) {
|
||||
if (media.type === 'photo') {
|
||||
images.push(media.media_url_https);
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
if (
|
||||
entry.content.tweet.extended_entities?.media &&
|
||||
entry.content.tweet.extended_entities.media.length > 0
|
||||
) {
|
||||
const images = [];
|
||||
|
||||
for (const media of entry.content.tweet.extended_entities.media) {
|
||||
if (media.type === 'photo') {
|
||||
images.push(media.media_url_https);
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
if (
|
||||
entry.content.tweet.entities.media &&
|
||||
entry.content.tweet.entities.media.length > 0
|
||||
) {
|
||||
const images = [];
|
||||
|
||||
for (const media of entry.content.tweet.entities.media) {
|
||||
if (media.type === 'photo') {
|
||||
images.push(media.media_url_https);
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* `Feed` is the interface for the returned data from Twitter for a users
|
||||
* timeline.
|
||||
*/
|
||||
export interface Feed {
|
||||
props: {
|
||||
pageProps: {
|
||||
timeline: {
|
||||
entries: Entry[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
content: {
|
||||
tweet: {
|
||||
created_at: string;
|
||||
entities: {
|
||||
media?: {
|
||||
media_url_https: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
full_text: string;
|
||||
id_str: string;
|
||||
permalink: string;
|
||||
user: {
|
||||
screen_name: string;
|
||||
profile_image_url_https: string;
|
||||
};
|
||||
retweeted_status?: {
|
||||
entities?: {
|
||||
media?: {
|
||||
media_url_https: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
extended_entities?: {
|
||||
media?: {
|
||||
media_url_https: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
media?: {
|
||||
media_url_https: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
extended_entities?: {
|
||||
media?: {
|
||||
media_url_https: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export type TSourceType =
|
||||
| 'rss'
|
||||
| 'stackoverflow'
|
||||
| 'tumblr'
|
||||
| 'x'
|
||||
| 'youtube'
|
||||
| 'none';
|
||||
|
||||
@@ -46,6 +45,5 @@ export interface ISourceOptions {
|
||||
rss?: string;
|
||||
stackoverflow?: ISourceOptionsStackOverflow;
|
||||
tumblr?: string;
|
||||
x?: string;
|
||||
youtube?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user