[ol]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/ol]
[list=1]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/list]
@@ -404,8 +404,8 @@ code
[list=]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/list]
@@ -416,8 +416,8 @@ code
[list=i]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/list]
@@ -428,8 +428,8 @@ code
[list=I]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/list]
@@ -440,8 +440,8 @@ code
[list=a]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/list]
@@ -452,8 +452,8 @@ code
[list=A]
- [*] First list element
- [*] Second list element
+ [li] First list element
+ [li] Second list element
[/list]
diff --git a/doc/Channels.md b/doc/Channels.md
index de3f6718d8..0f5413b1f1 100644
--- a/doc/Channels.md
+++ b/doc/Channels.md
@@ -25,10 +25,15 @@ Predefined Channels
* Posts from people you interact with on a more than average level.
* Posts from the accounts that you follow with a more than average number of interactions-
* Posts from accounts where you activated "notify on new posts" or where you have set the channel frequency accordingly.
+* Discover: Posts from contacts you don't follow, but that might be of interest for you to follow. In detail, it consists of:
+ * Posts from people you don't follow but you interact with on a more than average level.
+ * Posts from people you don't follow but that interact with you on a more than average level.
+ * Popular posts from people you don't follow but you interacted with or who interacted with you on any level.
* What's Hot: Posts with a more than average number of interactions.
* Language: Posts in your language.
* Followers: Posts from your followers that you don't follow.
* Sharers of sharers: Posts from accounts that are followed by accounts that you follow.
+* Quiet sharers: Posts from accounts that you follow but who don't post very often.
* Images: Posts with images.
* Audio: Posts with audio.
* Videos: Posts with videos.
@@ -52,34 +57,43 @@ Each channel is defined by these values:
Additional keywords for the full text search
---
-Additionally to the search for content, there are additional keywords that can be used in the full text search:
+Additionally to the search for content, there are keywords that can be used in the full text search.
+Alternatives are presented with "|".
* from - Use "from:nickname" or "from:nickname@domain.tld" to search for posts from a specific author.
* to - Use "from:nickname" or "from:nickname@domain.tld" to search for posts with the given contact as receiver.
-* group - Use "from:nickname" or "from:nickname@domain.tld" to search for group post of the given group.
+* group - Use "group:nickname" or "group:nickname@domain.tld" to search for group post of the given group.
+* application | relay - Use "application:nickname" or "application:nickname@domain.tld" to search for posts that had been reshared by the given relay application.
* server - Use "server:hostname" to search for posts from a specific server. In the case of group postings, the search text contains both the hostname of the group server and the author's hostname.
* source - The ActivityPub type of the post source. Use this for example to include or exclude group posts or posts from services (aka bots).
* source:person - The post is created by a regular user account.
* source:organization - The post is created by an organisation.
* source:group - The post is created by or distributed via a group.
- * source:service - The posts originates from a service account. This source type is often used to mark bot accounts.
- * source:application - The post is created by an application. This is most likely unused in the fediverse for post creation.
+ * source:service | source:news - The posts originates from a service account. This source type is often used to mark bot accounts.
+ * source:application | source:relay - The post is created by an application. This is most likely unused in the fediverse for post creation.
* tag - Use "tag:tagname" to search for a specific tag.
-* network - Use this to include or exclude some networks from your channel.
- * network:apub - ActivityPub (Used by the systems in the Fediverse)
- * network:dfrn - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub.
- * network:dspr - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
+* media - With this keyword you can search for attached media.
+ * media:image | media:photo | media:picture - The post contains an image
+ * media:video - The post contains a video
+ * media:audio - The post contains audio
+ * media:card - The post contains a link preview card
+ * media:post - The post links another post, means it is a quoted post
+* network | net - Use this to include or exclude some networks from your channel.
+ * network:apub | network:activitypub - ActivityPub (Used by the systems in the Fediverse)
+ * network:dfrn | network:friendica - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub.
+ * network:dspr | network:diaspora - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails that had been imported via IMAP.
- * network:stat - The OStatus protocol is mainly used by old GNU Social installations.
- * network:dscs - Posts that are received by the Discourse connector.
- * network:tmbl - Posts that are received by the Tumblr connector.
- * network:bsky - Posts that are received by the Bluesky connector.
+ * network:stat | network:ostatus - The OStatus protocol is mainly used by old GNU Social installations.
+ * network:dscs | network:discourse - Posts that are received by the Discourse connector.
+ * network:tmbl | network:tumblr - Posts that are received by the Tumblr connector.
+ * network:bsky | network:bluesky - Posts that are received by the Bluesky connector.
* platform - Use this to include or exclude some platforms from your channel, e.g. "+platform:friendica". In the case of group postings, the search text contains both the platform of the group server and the author's platform.
* visibility - You have the choice between different visibilities. You can only see unlisted or private posts that you have the access for.
* visibility:public
* visibility:unlisted
* visibility:private
+* language | lang - Use "language:code" to search for posts with the given language in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
Remember that you can combine these kerywords.
So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn"
\ No newline at end of file
diff --git a/doc/FAQ.md b/doc/FAQ.md
index ebb8c041b5..ea4bec68f7 100644
--- a/doc/FAQ.md
+++ b/doc/FAQ.md
@@ -178,12 +178,12 @@ The available features are client specific and may differ.
#### Android
* [AndStatus](http://andstatus.org) ([F-Droid](https://f-droid.org/repository/browse/?fdid=org.andstatus.app), [Google Play](https://play.google.com/store/apps/details?id=org.andstatus.app))
-* [Fedi](https://github.com/Big-Fig/Fediverse.app) ([Google Play](https://play.google.com/store/apps/details?id=com.fediverse.app))
* [Fedilab](https://fedilab.app) ([F-Droid](https://f-droid.org/app/fr.gouv.etalab.mastodon), [Google Play](https://play.google.com/store/apps/details?id=app.fedilab.android))
* [Friendiqa](https://git.friendi.ca/lubuwest/Friendiqa) ([F-Droid](https://git.friendi.ca/lubuwest/Friendiqa#install), [Google Play](https://play.google.com/store/apps/details?id=org.qtproject.friendiqa))
-* [Husky](https://git.sr.ht/~captainepoch/husky) ([F-Droid](https://f-droid.org/repository/browse/?fdid=su.xash.husky), [Google Play](https://play.google.com/store/apps/details?id=su.xash.husky))
+* [Husky](https://codeberg.org/husky/husky) ([F-Droid](https://f-droid.org/repository/browse/?fdid=su.xash.husky), [Google Play](https://play.google.com/store/apps/details?id=su.xash.husky))
* [Mastodon](https://github.com/mastodon/mastodon-android) ([F-Droid](https://f-droid.org/en/packages/org.joinmastodon.android/), [Google Play](https://play.google.com/store/apps/details?id=org.joinmastodon.android))
-* [Subway Tooter](https://github.com/tateisu/SubwayTooter) ([F-Droid](https://android.izzysoft.de/repo/apk/jp.juggler.subwaytooter))
+* [Pachli](https://pachli.app/) ([F-Droid](https://f-droid.org/en/packages/app.pachli/), [Google Play](https://play.google.com/store/apps/details?id=app.pachli))
+* [Subway Tooter](https://github.com/tateisu/SubwayTooter) ([F-Droid via Izzy](https://android.izzysoft.de/repo/apk/jp.juggler.subwaytooter.noFcm))
* [Tooot](https://tooot.app/) ([Google Play](https://play.google.com/store/apps/details?id=com.xmflsct.app.tooot))
* [Tusky](https://tusky.app) ([F-Droid](https://f-droid.org/repository/browse/?fdid=com.keylesspalace.tusky), [Google Play](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky))
* [TwidereX](https://github.com/TwidereProject/TwidereX-Android) ([F-Droid](https://f-droid.org/en/packages/com.twidere.twiderex/), [Google Play](https://play.google.com/store/apps/details?id=com.twidere.twiderex))
@@ -193,7 +193,7 @@ The available features are client specific and may differ.
* [Mastodon](https://joinmastodon.org/apps) ([App Store](https://apps.apple.com/us/app/mastodon-for-iphone/id1571998974))
* [Stella*](https://www.stella-app.net/) ([App Store](https://apps.apple.com/us/app/stella-for-mastodon-twitter/id921372048))
-* [Tooot](https://github.com/tooot-app) ([App Store](https://apps.apple.com/app/id1549772269)
+* [Tooot](https://github.com/tooot-app) ([App Store](https://apps.apple.com/app/id1549772269))
* [TwidereX](https://github.com/TwidereProject/TwidereX-iOS) ([App Store](https://apps.apple.com/app/twidere-x/id1530314034))
#### Linux
@@ -211,7 +211,7 @@ The available features are client specific and may differ.
#### Windows
* [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk))
-* [Whalebird](https://whalebird.social/en/desktop/contents) ([Website Download](https://whalebird.social/en/desktop/contents/downloads#windows), [GitHub](https://github.com/h3poteto/whalebird-desktop))
+* [Whalebird](https://whalebird.social/en/desktop/contents) ([Microsoft Store](https://apps.microsoft.com/detail/9nbw4csdv5hc), [GitHub](https://github.com/h3poteto/whalebird-desktop))
#### Web Frontend
diff --git a/doc/Install.md b/doc/Install.md
index 4715c27233..c50854aaf2 100644
--- a/doc/Install.md
+++ b/doc/Install.md
@@ -44,7 +44,7 @@ For alternative server configurations (such as Nginx server and MariaDB database
### Optional
-* PHP ImageMagick extension (php-imagick) for animated GIF support.
+* PHP ImageMagick extension (php-imagick) for animated GIF and animated WebP support.
## Installation procedure
diff --git a/doc/database.md b/doc/database.md
index 26519ccfb4..5942b1144d 100644
--- a/doc/database.md
+++ b/doc/database.md
@@ -61,6 +61,7 @@ Database Tables
| [post-category](help/database/db_post-category) | post relation to categories |
| [post-collection](help/database/db_post-collection) | Collection of posts |
| [post-content](help/database/db_post-content) | Content for all posts |
+| [post-counts](help/database/db_post-counts) | Original remote activity |
| [post-delivery](help/database/db_post-delivery) | Delivery data for posts for the batch processing |
| [post-delivery-data](help/database/db_post-delivery-data) | Delivery data for items |
| [post-engagement](help/database/db_post-engagement) | Engagement data per post |
@@ -69,6 +70,7 @@ Database Tables
| [post-media](help/database/db_post-media) | Attached media |
| [post-question](help/database/db_post-question) | Question |
| [post-question-option](help/database/db_post-question-option) | Question option |
+| [post-searchindex](help/database/db_post-searchindex) | Content for all posts |
| [post-tag](help/database/db_post-tag) | post relation to tags |
| [post-thread](help/database/db_post-thread) | Thread related data |
| [post-thread-user](help/database/db_post-thread-user) | Thread related data per user |
diff --git a/doc/database/db_channel.md b/doc/database/db_channel.md
index 9b9486fa32..4a469b4ee3 100644
--- a/doc/database/db_channel.md
+++ b/doc/database/db_channel.md
@@ -16,8 +16,13 @@ Fields
| access-key | Access key | varchar(1) | YES | | NULL | |
| include-tags | Comma separated list of tags that will be included in the channel | varchar(1023) | YES | | NULL | |
| exclude-tags | Comma separated list of tags that aren't allowed in the channel | varchar(1023) | YES | | NULL | |
+| min-size | Minimum post size | int unsigned | YES | | NULL | |
+| max-size | Maximum post size | int unsigned | YES | | NULL | |
| full-text-search | Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode | varchar(1023) | YES | | NULL | |
| media-type | Filtered media types | smallint unsigned | YES | | NULL | |
+| languages | Desired languages | mediumtext | YES | | NULL | |
+| publish | publish channel content | boolean | YES | | NULL | |
+| valid | Set, when the full-text-search is valid | boolean | YES | | NULL | |
Indexes
------------
diff --git a/doc/database/db_contact-relation.md b/doc/database/db_contact-relation.md
index f11fd95a0f..c83c9b8326 100644
--- a/doc/database/db_contact-relation.md
+++ b/doc/database/db_contact-relation.md
@@ -6,17 +6,18 @@ Contact relations
Fields
------
-| Field | Description | Type | Null | Key | Default | Extra |
-| --------------------- | -------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
-| cid | contact the related contact had interacted with | int unsigned | NO | PRI | 0 | |
-| relation-cid | related contact who had interacted with the contact | int unsigned | NO | PRI | 0 | |
-| last-interaction | Date of the last interaction by relation-cid on cid | datetime | NO | | 0001-01-01 00:00:00 | |
-| follow-updated | Date of the last update of the contact relationship | datetime | NO | | 0001-01-01 00:00:00 | |
-| follows | if true, relation-cid follows cid | boolean | NO | | 0 | |
-| score | score for interactions of cid on relation-cid | smallint unsigned | YES | | NULL | |
-| relation-score | score for interactions of relation-cid on cid | smallint unsigned | YES | | NULL | |
-| thread-score | score for interactions of cid on threads of relation-cid | smallint unsigned | YES | | NULL | |
-| relation-thread-score | score for interactions of relation-cid on threads of cid | smallint unsigned | YES | | NULL | |
+| Field | Description | Type | Null | Key | Default | Extra |
+| --------------------- | ----------------------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
+| cid | contact the related contact had interacted with | int unsigned | NO | PRI | 0 | |
+| relation-cid | related contact who had interacted with the contact | int unsigned | NO | PRI | 0 | |
+| last-interaction | Date of the last interaction by relation-cid on cid | datetime | NO | | 0001-01-01 00:00:00 | |
+| follow-updated | Date of the last update of the contact relationship | datetime | NO | | 0001-01-01 00:00:00 | |
+| follows | if true, relation-cid follows cid | boolean | NO | | 0 | |
+| score | score for interactions of cid on relation-cid | smallint unsigned | YES | | NULL | |
+| relation-score | score for interactions of relation-cid on cid | smallint unsigned | YES | | NULL | |
+| thread-score | score for interactions of cid on threads of relation-cid | smallint unsigned | YES | | NULL | |
+| relation-thread-score | score for interactions of relation-cid on threads of cid | smallint unsigned | YES | | NULL | |
+| post-score | score for the amount of posts from cid that can be seen by relation-cid | smallint unsigned | YES | | NULL | |
Indexes
------------
diff --git a/doc/database/db_post-content.md b/doc/database/db_post-content.md
index 27b372093a..d1ae90f278 100644
--- a/doc/database/db_post-content.md
+++ b/doc/database/db_post-content.md
@@ -17,6 +17,7 @@ Fields
| location | text location where this item originated | varchar(255) | NO | | | |
| coord | longitude/latitude pair representing location where this item originated | varchar(255) | NO | | | |
| language | Language information about this post | text | YES | | NULL | |
+| sensitive | If true, this post contains sensitive content | boolean | YES | | NULL | |
| app | application which generated this item | varchar(255) | NO | | | |
| rendered-hash | | varchar(32) | NO | | | |
| rendered-html | item.body converted to html | mediumtext | YES | | NULL | |
@@ -30,13 +31,12 @@ Fields
Indexes
------------
-| Name | Fields |
-| -------------------------- | -------------------------------------- |
-| PRIMARY | uri-id |
-| plink | plink(191) |
-| resource-id | resource-id |
-| title-content-warning-body | FULLTEXT, title, content-warning, body |
-| quote-uri-id | quote-uri-id |
+| Name | Fields |
+| ------------ | ------------ |
+| PRIMARY | uri-id |
+| plink | plink(191) |
+| resource-id | resource-id |
+| quote-uri-id | quote-uri-id |
Foreign Keys
------------
diff --git a/doc/database/db_post-counts.md b/doc/database/db_post-counts.md
new file mode 100644
index 0000000000..db2a8fd36d
--- /dev/null
+++ b/doc/database/db_post-counts.md
@@ -0,0 +1,35 @@
+Table post-counts
+===========
+
+Original remote activity
+
+Fields
+------
+
+| Field | Description | Type | Null | Key | Default | Extra |
+| ------------- | ----------------------------------------------------------- | ----------------- | ---- | --- | ------- | ----- |
+| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
+| vid | Id of the verb table entry that contains the activity verbs | smallint unsigned | NO | PRI | NULL | |
+| reaction | Emoji Reaction | varchar(4) | NO | PRI | NULL | |
+| parent-uri-id | Id of the item-uri table that contains the parent uri | int unsigned | YES | | NULL | |
+| count | Number of activities | int unsigned | YES | | 0 | |
+
+Indexes
+------------
+
+| Name | Fields |
+| ------------- | --------------------- |
+| PRIMARY | uri-id, vid, reaction |
+| vid | vid |
+| parent-uri-id | parent-uri-id |
+
+Foreign Keys
+------------
+
+| Field | Target Table | Target Field |
+|-------|--------------|--------------|
+| uri-id | [item-uri](help/database/db_item-uri) | id |
+| vid | [verb](help/database/db_verb) | id |
+| parent-uri-id | [item-uri](help/database/db_item-uri) | id |
+
+Return to [database documentation](help/database)
diff --git a/doc/database/db_post-engagement.md b/doc/database/db_post-engagement.md
index edca447f3d..32f3ce76b6 100644
--- a/doc/database/db_post-engagement.md
+++ b/doc/database/db_post-engagement.md
@@ -12,9 +12,11 @@ Fields
| owner-id | Item owner | int unsigned | NO | | 0 | |
| contact-type | Person, organisation, news, community, relay | tinyint | NO | | 0 | |
| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | |
-| language | Language information about this post | varbinary(128) | YES | | NULL | |
+| language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
+| size | Body size | int unsigned | YES | | NULL | |
| created | | datetime | YES | | NULL | |
+| network | | char(4) | YES | | NULL | |
| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
| comments | Number of comments | mediumint unsigned | YES | | NULL | |
| activities | Number of activities (like, dislike, ...) | mediumint unsigned | YES | | NULL | |
diff --git a/doc/database/db_post-searchindex.md b/doc/database/db_post-searchindex.md
new file mode 100644
index 0000000000..c6504a7ed3
--- /dev/null
+++ b/doc/database/db_post-searchindex.md
@@ -0,0 +1,38 @@
+Table post-searchindex
+===========
+
+Content for all posts
+
+Fields
+------
+
+| Field | Description | Type | Null | Key | Default | Extra |
+| ---------- | --------------------------------------------------------------------- | ------------ | ---- | --- | ------- | ----- |
+| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
+| owner-id | Item owner | int unsigned | NO | | 0 | |
+| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | |
+| language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | |
+| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
+| size | Body size | int unsigned | YES | | NULL | |
+| created | | datetime | YES | | NULL | |
+| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
+
+Indexes
+------------
+
+| Name | Fields |
+| ---------- | -------------------- |
+| PRIMARY | uri-id |
+| owner-id | owner-id |
+| created | created |
+| searchtext | FULLTEXT, searchtext |
+
+Foreign Keys
+------------
+
+| Field | Target Table | Target Field |
+|-------|--------------|--------------|
+| uri-id | [item-uri](help/database/db_item-uri) | id |
+| owner-id | [contact](help/database/db_contact) | id |
+
+Return to [database documentation](help/database)
diff --git a/doc/database/db_profile.md b/doc/database/db_profile.md
index c4a8b01709..09f10770be 100644
--- a/doc/database/db_profile.md
+++ b/doc/database/db_profile.md
@@ -56,11 +56,10 @@ Fields
Indexes
------------
-| Name | Fields |
-| -------------- | ---------------------- |
-| PRIMARY | id |
-| uid_is-default | uid, is-default |
-| pub_keywords | FULLTEXT, pub_keywords |
+| Name | Fields |
+| -------------- | --------------- |
+| PRIMARY | id |
+| uid_is-default | uid, is-default |
Foreign Keys
------------
diff --git a/doc/database/db_user.md b/doc/database/db_user.md
index c1aefd6bee..108a689ad1 100644
--- a/doc/database/db_user.md
+++ b/doc/database/db_user.md
@@ -34,8 +34,6 @@ Fields
| blockwall | Prohibit contacts to post to the profile page of the user | boolean | NO | | 0 | |
| hidewall | Hide profile details from unknown viewers | boolean | NO | | 0 | |
| blocktags | Prohibit contacts to tag the post of this user | boolean | NO | | 0 | |
-| unkmail | Permit unknown people to send private mails to this user | boolean | NO | | 0 | |
-| cntunkmail | | int unsigned | NO | | 10 | |
| notify-flags | email notification options | smallint unsigned | NO | | 65535 | |
| page-flags | page/profile type | tinyint unsigned | NO | | 0 | |
| account-type | | tinyint unsigned | NO | | 0 | |
diff --git a/doc/de/BBCode.md b/doc/de/BBCode.md
index a6df8b67b9..068af666d1 100644
--- a/doc/de/BBCode.md
+++ b/doc/de/BBCode.md
@@ -356,8 +356,8 @@ Zeilen
diff --git a/doc/themes.md b/doc/themes.md
index 8d85dce2a9..5804868836 100644
--- a/doc/themes.md
+++ b/doc/themes.md
@@ -2,153 +2,10 @@
* [Home](help)
-To change the look of friendica you have to touch the themes.
-The current default theme is [Vier](https://github.com/friendica/friendica/tree/stable/view/theme/vier) but there are numerous others.
-Have a look at [github.com/bkil/friendica-themes](https://github.com/bkil/friendica-themes) for an overview of the existing themes.
-In case none of them suits your needs, there are several ways to change a theme.
+The default Theme in Friendica is called [frio](https://github.com/friendica/friendica/tree/stable/view/theme/frio).
-So, how to work on the UI of friendica.
+Open `Settings > Display > Custom Theme Settings` adjust the Theme to your liking. Select your preferred Appearance - light, dark or black - and your favorite Accent color. Click `Submit` to save your changes.
-You can either directly edit an existing theme.
-But you might loose your changes when the theme is updated by the friendica team.
+The `Custom` Appearance allows to tweak the themes CSS and set colors for UI elements. So called `schemestrings` can be shared between users.
-If you are almost happy with an existing theme, the easiest way to cover your needs is to create a new theme, inheriting most of the properties of the parent theme and change just minor stuff.
-The below for a more detailed description of theme heritage.
-
-Some themes also allow users to select *variants* of the theme.
-Those theme variants most often contain an additional [CSS](https://en.wikipedia.org/wiki/CSS) file to override some styling of the default theme values.
-From the themes in the main repository *vier* and *vier* are using this methods for variations.
-Quattro is using a slightly different approach.
-
-Third you can start your theme from scratch.
-Which is the most complex way to change friendicas look.
-But it leaves you the most freedom.
-So below for a *detailed* description and the meaning of some special files.
-
-### Styling
-
-If you want to change the styling of a theme, have a look at the themes CSS file.
-In most cases, you can found these in
-
- /view/theme/**your-theme-name**/style.css
-
-sometimes, there is also a file called style.php in the theme directory.
-This is only needed if the theme allows the user to change certain things of the theme dynamically.
-Say the font size or set a background image.
-
-### Templates
-
-If you want to change the structure of the theme, you need to change the templates used by the theme.
-Friendica themes are using [SMARTY3](http://www.smarty.net/) for templating.
-The default template can be found in
-
- /view/templates
-
-if you want to override any template within your theme create your version of the template in
-
- /view/theme/**your-theme-name**/templates
-
-any template that exists there will be used instead of the default one.
-
-### JavaScript
-
-The same rule applies to the JavaScript files found in
-
- /js
-
-they will be overwritten by files in
-
- /view/theme/**your-theme-name**/js.
-
-## Creating a Theme from Scratch
-
-Keep patient.
-Basically what you have to do is identify which template you have to change so it looks more like what you want.
-Adopt the CSS of the theme accordingly.
-And iterate the process until you have the theme the way you want it.
-
-*Use the source Luke.* and don't hesitate to ask in @[developers](https://forum.friendi.ca/profile/developers) or @[helpers](https://forum.friendi.ca/profile/helpers).
-
-## Special Files
-
-### unsupported
-
-If a file with this name (which might be empty) exists in the theme directory, the theme is marked as *unsupported*.
-An unsupported theme may not be selected by a user in the settings.
-Users who are already using it wont notice anything.
-
-### README(.md)
-
-The contents of this file, with or without the .md which indicates [Markdown](https://daringfireball.net/projects/markdown/) syntax, will be displayed at most repository hosting services and in the theme page within the admin panel of friendica.
-
-This file should contain information you want to let others know about your theme.
-
-### screenshot.[png|jpg]
-
-If you want to have a preview image of your theme displayed in the settings you should take a screenshot and save it with this name.
-Supported formats are PNG and JPEG.
-
-### theme.php
-
-This is the main definition file of the theme.
-In the header of that file, some meta information is stored.
-For example, have a look at the theme.php of the *vier* theme:
-
-
- * Author: Ike
- * Author: Beanow
- * Maintainer: Ike
- * Description: "Vier" is a very compact and modern theme. It uses the font awesome font library: http://fortawesome.github.com/Font-Awesome/
- */
-
-You see the definition of the theme's name, it's version and the initial author of the theme.
-These three pieces of information should be listed.
-If the original author is no longer working on the theme, but a maintainer has taken over, the maintainer should be listed as well.
-The information from the theme header will be displayed in the admin panel.
-
-The first thing in file is to import the `App` class from `\Friendica\` namespace.
-
- use Friendica\App;
-
-This will make our job a little easier, as we don't have to specify the full name every time we need to use the `App` class.
-
-The next crucial part of the theme.php file is a definition of an init function.
-The name of the function is _init.
-So in the case of vier it is
-
- function vier_init(App $a) {
- $a->theme_info = array();
- $a->set_template_engine('smarty3');
- }
-
-Here we have set the basic theme information, in this case they are empty.
-But the array needs to be set.
-And we have set the template engine that should be used by friendica for this theme.
-At the moment you should use the *smarty3* engine.
-There once was a friendica specific templating engine as well but that is not used anymore.
-If you like to use another templating engine, please implement it.
-
-If you want to add something to the HTML header of the theme, one way to do so is by adding it to the theme.php file.
-To do so, add something alike
-
- DI::page()['htmlhead'] .= <<< EOT
- /* stuff you want to add to the header */
- EOT;
-
-So you can access the properties of this friendica session from the theme.php file as well.
-
-### default.php
-
-This file covers the structure of the underlying HTML layout.
-The default file is in
-
- /view/default.php
-
-if you want to change it, say adding a 4th column for banners of your favourite FLOSS projects, place a new default.php file in your theme directory.
-As with the theme.php file, you can use the properties of the $a variable with holds the friendica application to decide what content is displayed.
+In the `General Theme Settings` you can also find the [vier](https://github.com/friendica/friendica/tree/stable/view/theme/vier) Theme, which precedes frio and is no longer officially maintained.
diff --git a/images/bluesky.jpg b/images/bluesky.jpg
index 35020ac770..4f551764c4 100644
Binary files a/images/bluesky.jpg and b/images/bluesky.jpg differ
diff --git a/images/screenshots/friendica-2023-10-frio-desktop.png b/images/screenshots/friendica-2023-10-frio-desktop.png
deleted file mode 100644
index e06f971365..0000000000
Binary files a/images/screenshots/friendica-2023-10-frio-desktop.png and /dev/null differ
diff --git a/images/screenshots/friendica-2023-12-frio-desktop.png b/images/screenshots/friendica-2023-12-frio-desktop.png
new file mode 100644
index 0000000000..1a926ffb65
Binary files /dev/null and b/images/screenshots/friendica-2023-12-frio-desktop.png differ
diff --git a/index.php b/index.php
index 87778308a5..1b62b71c1c 100644
--- a/index.php
+++ b/index.php
@@ -1,6 +1,6 @@
render([$post], Conversation::MODE_SEARCH, false, true);
diff --git a/mod/lostpass.php b/mod/lostpass.php
index 5eb53ae2f3..51e0ba8db6 100644
--- a/mod/lostpass.php
+++ b/mod/lostpass.php
@@ -1,6 +1,6 @@
t('User not found.'));
}
- $phototypes = Images::supportedTypes();
-
$can_post = false;
$visitor = 0;
@@ -215,14 +213,14 @@ function photos_post(App $a)
// get the list of photos we are about to delete
if ($visitor) {
$r = DBA::toArray(DBA::p(
- "SELECT distinct(`resource-id`) as `rid` FROM `photo` WHERE `contact-id` = ? AND `uid` = ? AND `album` = ?",
+ "SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `contact-id` = ? AND `uid` = ? AND `album` = ?",
$visitor,
$page_owner_uid,
$album
));
} else {
$r = DBA::toArray(DBA::p(
- "SELECT distinct(`resource-id`) as `rid` FROM `photo` WHERE `uid` = ? AND `album` = ?",
+ "SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `uid` = ? AND `album` = ?",
DI::userSession()->getLocalUserId(),
$album
));
@@ -337,7 +335,7 @@ function photos_post(App $a)
if (DBA::isResult($photos)) {
$photo = $photos[0];
- $ext = $phototypes[$photo['type']];
+ $ext = Images::getExtensionByMimeType($photo['type']);
Photo::update(
['desc' => $desc, 'album' => $albname, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny],
['resource-id' => $resource_id, 'uid' => $page_owner_uid]
@@ -590,8 +588,6 @@ function photos_content(App $a)
$profile = Profile::getByUID($user['uid']);
- $phototypes = Images::supportedTypes();
-
$_SESSION['photo_return'] = DI::args()->getCommand();
// Parse arguments
@@ -762,7 +758,7 @@ function photos_content(App $a)
$total = 0;
$r = DBA::toArray(DBA::p(
- "SELECT `resource-id`, max(`scale`) AS `scale` FROM `photo` WHERE `uid` = ? AND `album` = ?
+ "SELECT `resource-id`, MAX(`scale`) AS `scale` FROM `photo` WHERE `uid` = ? AND `album` = ?
AND `scale` <= 4 $sql_extra GROUP BY `resource-id`",
$owner_uid,
$album
@@ -782,9 +778,9 @@ function photos_content(App $a)
}
$r = DBA::toArray(DBA::p(
- "SELECT `resource-id`, ANY_VALUE(`id`) AS `id`, ANY_VALUE(`filename`) AS `filename`,
- ANY_VALUE(`type`) AS `type`, max(`scale`) AS `scale`, ANY_VALUE(`desc`) as `desc`,
- ANY_VALUE(`created`) as `created`
+ "SELECT `resource-id`, MIN(`id`) AS `id`, MIN(`filename`) AS `filename`,
+ MIN(`type`) AS `type`, MAX(`scale`) AS `scale`, MIN(`desc`) AS `desc`,
+ MIN(`created`) AS `created`
FROM `photo` WHERE `uid` = ? AND `album` = ?
AND `scale` <= 4 $sql_extra GROUP BY `resource-id` ORDER BY `created` $order LIMIT ? , ?",
intval($owner_uid),
@@ -844,7 +840,7 @@ function photos_content(App $a)
foreach ($r as $rr) {
$twist = !$twist;
- $ext = $phototypes[$rr['type']];
+ $ext = Images::getExtensionByMimeType($rr['type']);
$imgalt_e = $rr['filename'];
$desc_e = $rr['desc'];
@@ -855,7 +851,7 @@ function photos_content(App $a)
'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id']
. ($order_field === 'created' ? '?order=created' : ''),
'title' => DI::l10n()->t('View Photo'),
- 'src' => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . '.' . $ext,
+ 'src' => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . $ext,
'alt' => $imgalt_e,
'desc' => $desc_e,
'ext' => $ext,
@@ -1013,9 +1009,9 @@ function photos_content(App $a)
}
$photo = [
- 'href' => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . '.' . $phototypes[$hires['type']],
+ 'href' => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . Images::getExtensionByMimeType($hires['type']),
'title' => DI::l10n()->t('View Full Size'),
- 'src' => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . '.' . $phototypes[$lores['type']] . '?_u=' . DateTimeFormat::utcNow('ymdhis'),
+ 'src' => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . Images::getExtensionByMimeType($lores['type']) . '?_u=' . DateTimeFormat::utcNow('ymdhis'),
'height' => $hires['height'],
'width' => $hires['width'],
'album' => $hires['album'],
@@ -1043,7 +1039,7 @@ function photos_content(App $a)
$pager = new Pager(DI::l10n(), DI::args()->getQueryString());
$params = ['order' => ['id'], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
- $items = Post::toArray(Post::selectForUser($link_item['uid'], Item::ITEM_FIELDLIST, $condition, $params));
+ $items = Post::toArray(Post::selectForUser($link_item['uid'], array_merge(Item::ITEM_FIELDLIST, ['author-alias']), $condition, $params));
if (DI::userSession()->getLocalUserId() == $link_item['uid']) {
Item::update(['unseen' => false], ['parent' => $link_item['parent']]);
@@ -1167,11 +1163,11 @@ function photos_content(App $a)
}
if (!empty($conv_responses['like'][$link_item['uri']])) {
- $like = DI::conversation()->formatActivity($conv_responses['like'][$link_item['uri']]['links'], 'like', $link_item['id']);
+ $like = DI::conversation()->formatActivity($conv_responses['like'][$link_item['uri']]['links'], 'like', $link_item['id'], '', []);
}
if (!empty($conv_responses['dislike'][$link_item['uri']])) {
- $dislike = DI::conversation()->formatActivity($conv_responses['dislike'][$link_item['uri']]['links'], 'dislike', $link_item['id']);
+ $dislike = DI::conversation()->formatActivity($conv_responses['dislike'][$link_item['uri']]['links'], 'dislike', $link_item['id'], '', []);
}
if (($can_post || Security::canWriteToUserWall($owner_uid))) {
diff --git a/mod/update_contact.php b/mod/update_contact.php
index b97e9ec7be..474b545040 100644
--- a/mod/update_contact.php
+++ b/mod/update_contact.php
@@ -1,6 +1,6 @@
.
- *
- */
-
-if (($_POST["friendica_acct_name"] != '') && ($_POST["friendica_password"] != '')) {
- setcookie("username", $_POST["friendica_acct_name"], time()+60*60*24*300);
- setcookie("password", $_POST["friendica_password"], time()+60*60*24*300);
-}
-
-?>
-
-
-
-
-
-
- $content];
-
- // echo "posting to: $url ";
-
- $c = curl_init();
- curl_setopt($c, CURLOPT_URL, $url);
- curl_setopt($c, CURLOPT_USERPWD, "$username:$password");
- curl_setopt($c, CURLOPT_POSTFIELDS, $data);
- curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
- $c_result = curl_exec($c);
- if(curl_errno($c)){
- $error = curl_error($c);
- showForm($error, $content);
- }
-
- curl_close($c);
- if (!isset($error)) {
- echo '';
- }
-
- } else {
- $error = "Missing account name and/or password...try again please";
- showForm($error, $content);
- }
-
-} else {
- showForm(null, $content);
-}
-
-function showForm($error, $content) {
- $username_cookie = $_COOKIE['username'];
- $password_cookie = $_COOKIE['password'];
-
- echo <<
-
- Friendica Bookmarklet
-
-
-
-
-
-
-EOF;
-
-}
-?>
-
-
-
\ No newline at end of file
diff --git a/mods/fpostit/friendica.svg b/mods/fpostit/friendica.svg
deleted file mode 100644
index efbb051cd8..0000000000
--- a/mods/fpostit/friendica.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/local.config.ci.php b/mods/local.config.ci.php
index 2da6b2a1ab..98e6799972 100644
--- a/mods/local.config.ci.php
+++ b/mods/local.config.ci.php
@@ -1,6 +1,6 @@
out('3', false);
- $image = new Image($imgdata, Images::getMimeTypeByData($imgdata));
+ $image = new Image($imgdata);
if (!$image->isValid()) {
$this->out(' ' . $this->l10n->t('invalid image for id %s', $resourceid) . ' ', false);
$valid = false;
diff --git a/src/Console/PhpToPo.php b/src/Console/PhpToPo.php
index a400fea23c..2fbfaefaa9 100644
--- a/src/Console/PhpToPo.php
+++ b/src/Console/PhpToPo.php
@@ -1,6 +1,6 @@
getArgument(1);
$apcontact = APContact::getByURL($actor);
- if (empty($apcontact) || !in_array($apcontact['type'], ['Application', 'Service'])) {
- $this->out($actor . ' is no relay actor');
+ if (empty($apcontact)) {
+ $this->out($actor . ' wasn\'t found');
return 1;
}
if ($mode == 'add') {
+ if (!APContact::isRelay($apcontact)) {
+ $this->out($actor . ' is no relay actor');
+ return 1;
+ }
+
if (Transmitter::sendRelayFollow($actor)) {
$this->out('Successfully added ' . $actor);
} else {
diff --git a/src/Console/Relocate.php b/src/Console/Relocate.php
index 22de2903c5..2622958108 100644
--- a/src/Console/Relocate.php
+++ b/src/Console/Relocate.php
@@ -1,6 +1,6 @@
[] [-h|--help|-?] [-v]
- bin/console user add [ [ [ []]]] [-h|--help|-?] [-v]
+ bin/console user add [ [ [ [ []]]]] [-h|--help|-?] [-v]
bin/console user delete [] [-y] [-h|--help|-?] [-v]
bin/console user allow [] [-h|--help|-?] [-v]
bin/console user deny [] [-h|--help|-?] [-v]
@@ -228,10 +228,11 @@ HELP;
*/
private function addUser()
{
- $name = $this->getArgument(1);
- $nick = $this->getArgument(2);
- $email = $this->getArgument(3);
- $lang = $this->getArgument(4);
+ $name = $this->getArgument(1);
+ $nick = $this->getArgument(2);
+ $email = $this->getArgument(3);
+ $lang = $this->getArgument(4);
+ $avatar = $this->getArgument(5);
if (empty($name)) {
$this->out($this->l10n->t('Enter user name: '));
@@ -262,10 +263,15 @@ HELP;
$lang = CliPrompt::prompt();
}
+ if (empty($avatar)) {
+ $this->out($this->l10n->t('Enter URL of an image to use as avatar (optional): '));
+ $avatar = CliPrompt::prompt();
+ }
+
if (empty($lang)) {
return UserModel::createMinimal($name, $email, $nick);
} else {
- return UserModel::createMinimal($name, $email, $nick, $lang);
+ return UserModel::createMinimal($name, $email, $nick, $lang, $avatar);
}
}
diff --git a/src/Contact/Avatar.php b/src/Contact/Avatar.php
index fc4b7e38cb..f165e1d3a1 100644
--- a/src/Contact/Avatar.php
+++ b/src/Contact/Avatar.php
@@ -1,6 +1,6 @@
getBody();
+ if (!$fetchResult->isSuccess()) {
+ Logger::debug('Fetching was unsuccessful', ['avatar' => $avatar]);
+ return $fields;
+ }
+
+ $img_str = $fetchResult->getBodyString();
if (empty($img_str)) {
Logger::debug('Avatar is invalid', ['avatar' => $avatar]);
return $fields;
}
- $image = new Image($img_str, Images::getMimeTypeByData($img_str));
+ $image = new Image($img_str, $fetchResult->getContentType(), $avatar);
if (!$image->isValid()) {
Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]);
return $fields;
@@ -145,7 +150,7 @@ class Avatar
return '';
}
- $path = $filename . $size . '.' . $image->getExt();
+ $path = $filename . $size . $image->getExt();
$basepath = self::basePath();
if (empty($basepath)) {
diff --git a/src/Contact/FriendSuggest/Collection/FriendSuggests.php b/src/Contact/FriendSuggest/Collection/FriendSuggests.php
index 8777087bc9..a59a8f1e26 100644
--- a/src/Contact/FriendSuggest/Collection/FriendSuggests.php
+++ b/src/Contact/FriendSuggest/Collection/FriendSuggests.php
@@ -1,6 +1,6 @@
getBlocklist())) {
- continue;
- }
-
// Can we put this after the visibility check?
$this->builtinActivityPuller($item, $conv_responses);
@@ -694,29 +690,6 @@ class Conversation
return $threads;
}
- private function getBlocklist(): array
- {
- if (!$this->session->getLocalUserId()) {
- return [];
- }
-
- $str_blocked = str_replace(["\n", "\r"], ",", $this->pConfig->get($this->session->getLocalUserId(), 'system', 'blocked') ?? '');
- if (empty($str_blocked)) {
- return [];
- }
-
- $blocklist = [];
-
- foreach (explode(',', $str_blocked) as $entry) {
- $cid = Contact::getIdForURL(trim($entry), 0, false);
- if (!empty($cid)) {
- $blocklist[] = $cid;
- }
- }
-
- return $blocklist;
- }
-
/**
* Adds some information (Causer, post reason, direction) to the fetched post row.
*
@@ -893,6 +866,7 @@ class Conversation
$emojis = $this->getEmojis($uriids);
$quoteshares = $this->getQuoteShares($uriids);
+ $counts = $this->getCounts($uriids);
if (!$this->config->get('system', 'legacy_activities')) {
$condition = DBA::mergeConditions($condition, ["(`gravity` != ? OR `origin`)", ItemModel::GRAVITY_ACTIVITY]);
@@ -900,7 +874,7 @@ class Conversation
$condition = DBA::mergeConditions(
$condition,
- ["`uid` IN (0, ?) AND (NOT `vid` IN (?, ?, ?) OR `vid` IS NULL)", $uid, Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)]
+ ["`uid` IN (0, ?) AND (NOT `verb` IN (?, ?, ?) OR `verb` IS NULL)", $uid, Activity::FOLLOW, Activity::VIEW, Activity::READ]
);
$condition = DBA::mergeConditions($condition, ["(`uid` != ? OR `private` != ?)", 0, ItemModel::PRIVATE]);
@@ -1017,6 +991,7 @@ class Conversation
foreach ($items as $key => $row) {
$items[$key]['emojis'] = $emojis[$key] ?? [];
+ $items[$key]['counts'] = $counts[$key] ?? 0;
$items[$key]['quoteshares'] = $quoteshares[$key] ?? [];
$always_display = in_array($mode, [self::MODE_CONTACTS, self::MODE_CONTACT_POSTS]);
@@ -1050,6 +1025,16 @@ class Conversation
*/
private function getEmojis(array $uriids): array
{
+ $emojis = [];
+
+ foreach (Post\Counts::get(['parent-uri-id' => $uriids]) as $count) {
+ $emojis[$count['uri-id']][$count['reaction']]['emoji'] = $count['reaction'];
+ $emojis[$count['uri-id']][$count['reaction']]['verb'] = Verb::getByID($count['vid']);
+ $emojis[$count['uri-id']][$count['reaction']]['total'] = $count['count'];
+ $emojis[$count['uri-id']][$count['reaction']]['title'] = [];
+ }
+
+ // @todo The following code should be removed, once that we display activity authors on demand
$activity_emoji = [
Activity::LIKE => '👍',
Activity::DISLIKE => '👎',
@@ -1058,42 +1043,49 @@ class Conversation
Activity::ATTENDNO => '❌',
Activity::ANNOUNCE => '♻',
Activity::VIEW => '📺',
+ Activity::READ => '📖',
];
- $index_list = array_values($activity_emoji);
- $verbs = array_merge(array_keys($activity_emoji), [Activity::EMOJIREACT, Activity::POST]);
-
+ $verbs = array_merge(array_keys($activity_emoji), [Activity::EMOJIREACT, Activity::POST]);
$condition = DBA::mergeConditions(['parent-uri-id' => $uriids, 'gravity' => [ItemModel::GRAVITY_ACTIVITY, ItemModel::GRAVITY_COMMENT], 'verb' => $verbs], ["NOT `deleted`"]);
$separator = chr(255) . chr(255) . chr(255);
- $sql = "SELECT `thr-parent-id`, `body`, `verb`, `gravity`, COUNT(*) AS `total`, GROUP_CONCAT(REPLACE(`author-name`, '" . $separator . "', ' ') SEPARATOR '" . $separator . "' LIMIT 50) AS `title` FROM `post-view` WHERE " . array_shift($condition) . " GROUP BY `thr-parent-id`, `verb`, `body`, `gravity`";
-
- $emojis = [];
+ $sql = "SELECT `parent-uri-id`, `thr-parent-id`, `body`, `verb`, `gravity`, GROUP_CONCAT(REPLACE(`author-name`, '" . $separator . "', ' ') SEPARATOR '" . $separator . "' LIMIT 50) AS `title` FROM `post-view` WHERE " . array_shift($condition) . " GROUP BY `parent-uri-id`, `thr-parent-id`, `verb`, `body`, `gravity`";
$rows = DBA::p($sql, $condition);
while ($row = DBA::fetch($rows)) {
if ($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) {
- $row['verb'] = $row['body'] ? Activity::EMOJIREACT : $row['verb'];
- $emoji = $row['body'] ?: $activity_emoji[$row['verb']];
+ $emoji = $row['body'] ?: $activity_emoji[$row['verb']];
} else {
$emoji = '';
}
- if (!isset($index_list[$emoji])) {
- $index_list[] = $emoji;
+ if (isset($emojis[$row['thr-parent-id']][$emoji]['title'])) {
+ $emojis[$row['thr-parent-id']][$emoji]['title'] = array_unique(array_merge($emojis[$row['thr-parent-id']][$emoji]['title'] ?? [], explode($separator, $row['title'])));
}
- $index = array_search($emoji, $index_list);
-
- $emojis[$row['thr-parent-id']][$index]['emoji'] = $emoji;
- $emojis[$row['thr-parent-id']][$index]['verb'] = $row['verb'];
- $emojis[$row['thr-parent-id']][$index]['total'] = ($emojis[$row['thr-parent-id']][$index]['total'] ?? 0) + $row['total'];
- $emojis[$row['thr-parent-id']][$index]['title'] = array_unique(array_merge($emojis[$row['thr-parent-id']][$index]['title'] ?? [], explode($separator, $row['title'])));
}
DBA::close($rows);
return $emojis;
}
+ /**
+ * Fetch comment counts from the conversation
+ *
+ * @param array $uriids
+ * @return array
+ */
+ private function getCounts(array $uriids): array
+ {
+ $counts = [];
+
+ foreach (Post\Counts::get(['parent-uri-id' => $uriids, 'verb' => Activity::POST]) as $count) {
+ $counts[$count['parent-uri-id']] = ($counts[$count['parent-uri-id']] ?? 0) + $count['count'];
+ }
+
+ return $counts;
+ }
+
/**
* Fetch quote shares from the conversation
*
@@ -1274,16 +1266,10 @@ class Conversation
return $parents;
}
- $blocklist = $this->getBlocklist();
-
$item_array = [];
// Dedupes the item list on the uri to prevent infinite loops
foreach ($item_list as $item) {
- if (in_array($item['author-id'], $blocklist)) {
- continue;
- }
-
$item_array[$item['uri-id']] = $item;
}
@@ -1472,10 +1458,6 @@ class Conversation
continue;
}
- if (in_array($item['author-id'], $this->getBlocklist())) {
- continue;
- }
-
// prevent private email from leaking.
if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
continue;
diff --git a/src/Content/Conversation/Collection/Timelines.php b/src/Content/Conversation/Collection/Timelines.php
index da9c7c9e61..41607ee01e 100644
--- a/src/Content/Conversation/Collection/Timelines.php
+++ b/src/Content/Conversation/Collection/Timelines.php
@@ -1,6 +1,6 @@
code = $code;
$this->label = $label;
@@ -69,8 +84,13 @@ class Timeline extends \Friendica\BaseEntity
$this->uid = $uid;
$this->includeTags = $includeTags;
$this->excludeTags = $excludeTags;
+ $this->minSize = $minSize;
+ $this->maxSize = $maxSize;
$this->fullTextSearch = $fullTextSearch;
$this->mediaType = $mediaType;
$this->circle = $circle;
+ $this->languages = $languages;
+ $this->publish = $publish;
+ $this->valid = $valid;
}
}
diff --git a/src/Content/Conversation/Entity/UserDefinedChannel.php b/src/Content/Conversation/Entity/UserDefinedChannel.php
index 3d88c6666b..574d677e08 100644
--- a/src/Content/Conversation/Entity/UserDefinedChannel.php
+++ b/src/Content/Conversation/Entity/UserDefinedChannel.php
@@ -1,6 +1,6 @@
l10n->t('For you'), $this->l10n->t('Posts from contacts you interact with and who interact with you'), 'y'),
+ new ChannelEntity(ChannelEntity::DISCOVER, $this->l10n->t('Discover'), $this->l10n->t('Posts from accounts that you don\'t follow, but that you might like.'), 'o'),
new ChannelEntity(ChannelEntity::WHATSHOT, $this->l10n->t('What\'s Hot'), $this->l10n->t('Posts with a lot of interactions'), 'h'),
new ChannelEntity(ChannelEntity::LANGUAGE, $native, $this->l10n->t('Posts in %s', $native), 'g'),
new ChannelEntity(ChannelEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'),
new ChannelEntity(ChannelEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'),
+ new ChannelEntity(ChannelEntity::QUIETSHARERS, $this->l10n->t('Quiet sharers'), $this->l10n->t('Posts from accounts that you follow but who don\'t post very often'), 'q'),
new ChannelEntity(ChannelEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'),
new ChannelEntity(ChannelEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'),
new ChannelEntity(ChannelEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'),
@@ -54,6 +56,6 @@ final class Channel extends Timeline
public function isTimeline(string $selectedTab): bool
{
- return in_array($selectedTab, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE]);
+ return in_array($selectedTab, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::DISCOVER, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::QUIETSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE]);
}
}
diff --git a/src/Content/Conversation/Factory/Community.php b/src/Content/Conversation/Factory/Community.php
index bd86ce3a9f..5472bd2739 100644
--- a/src/Content/Conversation/Factory/Community.php
+++ b/src/Content/Conversation/Factory/Community.php
@@ -1,6 +1,6 @@
pConfig = $pConfig;
+ $this->config = $config;
}
/**
@@ -63,6 +66,11 @@ class UserDefinedChannel extends \Friendica\BaseRepository
return $Entities;
}
+ public function select(array $condition, array $params = []): UserDefinedChannels
+ {
+ return $this->_select($condition, $params);
+ }
+
/**
* Fetch a single user channel
*
@@ -122,8 +130,13 @@ class UserDefinedChannel extends \Friendica\BaseRepository
'circle' => $Channel->circle,
'include-tags' => $Channel->includeTags,
'exclude-tags' => $Channel->excludeTags,
+ 'min-size' => $Channel->minSize,
+ 'max-size' => $Channel->maxSize,
'full-text-search' => $Channel->fullTextSearch,
'media-type' => $Channel->mediaType,
+ 'languages' => !empty($Channel->languages) ? serialize($Channel->languages) : null,
+ 'publish' => $Channel->publish,
+ 'valid' => $this->isValid($Channel->fullTextSearch),
];
if ($Channel->code) {
@@ -139,40 +152,161 @@ class UserDefinedChannel extends \Friendica\BaseRepository
return $Channel;
}
+ private function isValid(string $searchtext): bool
+ {
+ if ($searchtext == '') {
+ return true;
+ }
+
+ return $this->db->select('check-full-text-search', [], ["`pid` = ? AND MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE)", getmypid(), Engagement::escapeKeywords($searchtext)]) !== false;
+ }
+
/**
- * Checks, if one of the user defined channels matches with the given search text
- * @todo To increase the performance, this functionality should be replaced with a single SQL call.
+ * Checks if one of the user-defined channels matches the given language or item text via full-text search
*
- * @param string $searchtext
+ * @param string $haystack
* @param string $language
* @return boolean
+ * @throws \Exception
*/
- public function match(string $searchtext, string $language): bool
+ public function match(string $haystack, string $language): bool
{
- if (!in_array($language, User::getLanguages())) {
- $this->logger->debug('Unwanted language found. No matched channel found.', ['language' => $language, 'searchtext' => $searchtext]);
+ $users = $this->db->selectToArray('user', ['uid'], $this->getUserCondition());
+ if (empty($users)) {
return false;
}
- $store = false;
- $this->db->insert('check-full-text-search', ['pid' => getmypid(), 'searchtext' => $searchtext], Database::INSERT_UPDATE);
- $channels = $this->db->select(self::$table_name, ['full-text-search', 'uid', 'label'], ["`full-text-search` != ? AND `circle` = ?", '', 0]);
- while ($channel = $this->db->fetch($channels)) {
- $channelsearchtext = $channel['full-text-search'];
- foreach (Engagement::KEYWORDS as $keyword) {
- $channelsearchtext = preg_replace('~(' . $keyword . ':.[\w@\.-]+)~', '"$1"', $channelsearchtext);
- }
- if ($this->db->exists('check-full-text-search', ["`pid` = ? AND MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE)", getmypid(), $channelsearchtext])) {
- if (in_array($language, $this->pConfig->get($channel['uid'], 'channel', 'languages', [User::getLanguageCode($channel['uid'])]))) {
- $store = true;
- $this->logger->debug('Matching channel found.', ['uid' => $channel['uid'], 'label' => $channel['label'], 'language' => $language, 'channelsearchtext' => $channelsearchtext, 'searchtext' => $searchtext]);
- break;
- }
+ $uids = array_column($users, 'uid');
+
+ $usercondition = ['uid' => $uids];
+ $condition = DBA::mergeConditions($usercondition, ["`languages` != ? AND `include-tags` = ? AND `full-text-search` = ? AND `circle` = ?", '', '', '', 0]);
+ foreach ($this->select($condition) as $channel) {
+ if (!empty($channel->languages) && in_array($language, $channel->languages)) {
+ return true;
}
}
- $this->db->close($channels);
- $this->db->delete('check-full-text-search', ['pid' => getmypid()]);
- return $store;
+ $search = '';
+ $condition = DBA::mergeConditions($usercondition, ["`full-text-search` != ? AND `circle` = ? AND `valid`", '', 0]);
+ foreach ($this->select($condition) as $channel) {
+ $search .= '(' . $channel->fullTextSearch . ') ';
+ }
+
+ return (new DisposableFullTextSearch($this->db, $haystack))->match(Engagement::escapeKeywords($search));
+ }
+
+ /**
+ * List the IDs of the relay/group users that have matching user-defined channels based on an item details
+ *
+ * @param string $searchtext
+ * @param string $language
+ * @param array $tags
+ * @param int $media_type
+ * @param int $owner_id
+ * @param int $reshare_id
+ * @return array
+ * @throws \Exception
+ */
+ public function getMatchingChannelUsers(string $searchtext, string $language, array $tags, int $media_type, int $owner_id, int $reshare_id): array
+ {
+ $condition = $this->getUserCondition();
+ $condition = DBA::mergeConditions($condition, ["`account-type` IN (?, ?) AND `uid` != ?", User::ACCOUNT_TYPE_RELAY, User::ACCOUNT_TYPE_COMMUNITY, 0]);
+ $users = $this->db->selectToArray('user', ['uid'], $condition);
+ if (empty($users)) {
+ return [];
+ }
+
+ if (!in_array($language, User::getLanguages())) {
+ $this->logger->debug('Unwanted language found. No matched channel found.', ['language' => $language, 'searchtext' => $searchtext]);
+ return [];
+ }
+
+ $disposableFullTextSearch = new DisposableFullTextSearch($this->db, $searchtext);
+
+ $filteredChannels = $this->select(['uid' => array_column($users, 'uid'), 'publish' => true, 'valid' => true])->filter(
+ function (Entity\UserDefinedChannel $channel) use ($owner_id, $reshare_id, $language, $tags, $media_type, $disposableFullTextSearch, $searchtext) {
+ static $uids = [];
+
+ // Filter out channels from already picked users
+ if (in_array($channel->uid, $uids)) {
+ return false;
+ }
+
+ if (
+ ($channel->circle ?? 0)
+ && !$this->inCircle($channel->circle, $channel->uid, $owner_id)
+ && !$this->inCircle($channel->circle, $channel->uid, $reshare_id)
+ ) {
+ return false;
+ }
+
+ if (!in_array($language, $channel->languages ?: User::getWantedLanguages($channel->uid))) {
+ return false;
+ }
+
+ if ($channel->includeTags && !$this->inTaglist($channel->includeTags, $tags)) {
+ return false;
+ }
+
+ if ($channel->excludeTags && $this->inTaglist($channel->excludeTags, $tags)) {
+ return false;
+ }
+
+ if ($channel->mediaType && !($channel->mediaType & $media_type)) {
+ return false;
+ }
+
+ if ($channel->fullTextSearch && !$disposableFullTextSearch->match(Engagement::escapeKeywords($channel->fullTextSearch))) {
+ return false;
+ }
+
+ $uids[] = $channel->uid;
+ $this->logger->debug('Matching channel found.', ['uid' => $channel->uid, 'label' => $channel->label, 'language' => $language, 'tags' => $tags, 'media_type' => $media_type, 'searchtext' => $searchtext]);
+
+ return true;
+ }
+ );
+
+ return $filteredChannels->column('uid');
+ }
+
+ private function inCircle(int $circleId, int $uid, int $cid): bool
+ {
+ if ($cid == 0) {
+ return false;
+ }
+
+ $account = Contact::selectFirstAccountUser(['id'], ['pid' => $cid, 'uid' => $uid]);
+ if (empty($account['id'])) {
+ return false;
+ }
+ return $this->db->exists('group_member', ['gid' => $circleId, 'contact-id' => $account['id']]);
+ }
+
+ private function inTaglist(string $tagList, array $tags): bool
+ {
+ if (empty($tags)) {
+ return false;
+ }
+ array_walk($tags, function (&$value) {
+ $value = mb_strtolower($value);
+ });
+ foreach (explode(',', $tagList) as $tag) {
+ if (in_array($tag, $tags)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function getUserCondition(): array
+ {
+ $condition = ["`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` AND `user`.`uid` > ?", 0];
+
+ $abandon_days = intval($this->config->get('system', 'account_abandon_days'));
+ if (!empty($abandon_days)) {
+ $condition = DBA::mergeConditions($condition, ["`last-activity` > ?", DateTimeFormat::utc('now - ' . $abandon_days . ' days')]);
+ }
+ return $condition;
}
}
diff --git a/src/Content/Feature.php b/src/Content/Feature.php
index b9b9d2e0e6..a95d3779d3 100644
--- a/src/Content/Feature.php
+++ b/src/Content/Feature.php
@@ -1,6 +1,6 @@
$baseurl . '/' . $contact['id'],
+ 'url' => 'contact/' . $contact['id'] . '/conversations',
'external_url' => Contact::magicLinkByContact($contact),
'name' => $contact['name'],
'cid' => $contact['id'],
- 'selected' => $selected,
'micro' => DI::baseUrl()->remove(Contact::getMicro($contact)),
'id' => ++$id,
];
diff --git a/src/Content/Image.php b/src/Content/Image.php
index cc2fd5122c..a3a2e1abcc 100644
--- a/src/Content/Image.php
+++ b/src/Content/Image.php
@@ -1,6 +1,6 @@
$item['author-link'],
'alias' => $item['author-alias'],
];
- $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
+ $profile_link = Contact::magicLinkByContact($author, Contact::getProfileLink($author));
if (strpos($profile_link, 'contact/redir/') === 0) {
$status_link = $profile_link . '?' . http_build_query(['url' => $item['author-link'] . '/status']);
$photos_link = $profile_link . '?' . http_build_query(['url' => $item['author-link'] . '/photos']);
@@ -695,7 +695,7 @@ class Item
$item['body'] = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']);
}
- $shared_content = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], $item['plink'], $item['created'], $item['guid'], $item['uri']);
+ $shared_content = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], $item['plink'] ?? $item['uri'], $item['created'], $item['guid'], $item['uri']);
if (!empty($item['title'])) {
$shared_content .= '[h3]' . $item['title'] . "[/h3]\n";
@@ -1105,4 +1105,35 @@ class Item
Tag::store($toUriId, $receiver['type'], $receiver['name'], $receiver['url']);
}
}
+
+ /**
+ * Check if the item is too old
+ *
+ * @param string $created
+ * @param integer $uid
+ * @return boolean item is too old
+ */
+ public function isTooOld(string $created, int $uid = 0): bool
+ {
+ // check for create date and expire time
+ $expire_interval = DI::config()->get('system', 'dbclean-expire-days', 0);
+
+ if ($uid) {
+ $user = DBA::selectFirst('user', ['expire'], ['uid' => $uid]);
+ if (DBA::isResult($user) && ($user['expire'] > 0) && (($user['expire'] < $expire_interval) || ($expire_interval == 0))) {
+ $expire_interval = $user['expire'];
+ }
+ }
+
+ if (($expire_interval > 0) && !empty($created)) {
+ $expire_date = time() - ($expire_interval * 86400);
+ $created_date = strtotime($created);
+ if ($created_date < $expire_date) {
+ Logger::notice('Item created before expiration interval.', ['created' => date('c', $created_date), 'expired' => date('c', $expire_date)]);
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/Content/Nav.php b/src/Content/Nav.php
index cab5f7a425..a8f568630f 100644
--- a/src/Content/Nav.php
+++ b/src/Content/Nav.php
@@ -1,6 +1,6 @@
fetchFull($href . '&maxwidth=' . $a->getThemeInfoValue('videowidth'));
- if ($result->getReturnCode() === 200) {
- $json_string = $result->getBody();
+ if ($result->isSuccess()) {
+ $json_string = $result->getBodyString();
break;
}
}
@@ -157,57 +139,55 @@ class OEmbed
}
// Improve the OEmbed data with data from OpenGraph, Twitter cards and other sources
- if ($use_parseurl) {
- $data = ParseUrl::getSiteinfoCached($embedurl, false);
+ $data = ParseUrl::getSiteinfoCached($embedurl);
- if (($oembed->type == 'error') && empty($data['title']) && empty($data['text'])) {
- return $oembed;
- }
+ if (($oembed->type == 'error') && empty($data['title']) && empty($data['text'])) {
+ return $oembed;
+ }
- if ($no_rich_type || ($oembed->type == 'error')) {
- $oembed->html = '';
- $oembed->type = $data['type'];
+ if (!self::isAllowedURL($embedurl) || ($oembed->type == 'error')) {
+ $oembed->html = '';
+ $oembed->type = $data['type'];
- if ($oembed->type == 'photo') {
- if (!empty($data['images'])) {
- $oembed->url = $data['images'][0]['src'];
- $oembed->width = $data['images'][0]['width'];
- $oembed->height = $data['images'][0]['height'];
- } else {
- $oembed->type = 'link';
- }
+ if ($oembed->type == 'photo') {
+ if (!empty($data['images'])) {
+ $oembed->url = $data['images'][0]['src'];
+ $oembed->width = $data['images'][0]['width'];
+ $oembed->height = $data['images'][0]['height'];
+ } else {
+ $oembed->type = 'link';
}
}
+ }
- if (!empty($data['title'])) {
- $oembed->title = $data['title'];
- }
+ if (!empty($data['title'])) {
+ $oembed->title = $data['title'];
+ }
- if (!empty($data['text'])) {
- $oembed->description = $data['text'];
- }
+ if (!empty($data['text'])) {
+ $oembed->description = $data['text'];
+ }
- if (!empty($data['publisher_name'])) {
- $oembed->provider_name = $data['publisher_name'];
- }
+ if (!empty($data['publisher_name'])) {
+ $oembed->provider_name = $data['publisher_name'];
+ }
- if (!empty($data['publisher_url'])) {
- $oembed->provider_url = $data['publisher_url'];
- }
+ if (!empty($data['publisher_url'])) {
+ $oembed->provider_url = $data['publisher_url'];
+ }
- if (!empty($data['author_name'])) {
- $oembed->author_name = $data['author_name'];
- }
+ if (!empty($data['author_name'])) {
+ $oembed->author_name = $data['author_name'];
+ }
- if (!empty($data['author_url'])) {
- $oembed->author_url = $data['author_url'];
- }
+ if (!empty($data['author_url'])) {
+ $oembed->author_url = $data['author_url'];
+ }
- if (!empty($data['images']) && ($oembed->type != 'photo')) {
- $oembed->thumbnail_url = $data['images'][0]['src'];
- $oembed->thumbnail_width = $data['images'][0]['width'];
- $oembed->thumbnail_height = $data['images'][0]['height'];
- }
+ if (!empty($data['images']) && ($oembed->type != 'photo')) {
+ $oembed->thumbnail_url = $data['images'][0]['src'];
+ $oembed->thumbnail_width = $data['images'][0]['width'];
+ $oembed->thumbnail_height = $data['images'][0]['height'];
}
Hook::callAll('oembed_fetch_url', $embedurl, $oembed);
@@ -219,9 +199,10 @@ class OEmbed
* Returns a formatted string from OEmbed object
*
* @param \Friendica\Object\OEmbed $oembed
+ * @param int $uriid
* @return string
*/
- private static function formatObject(\Friendica\Object\OEmbed $oembed): string
+ private static function formatObject(\Friendica\Object\OEmbed $oembed, int $uriid): string
{
$ret = '