Compare commits

...
Author SHA1 Message Date
Prospector 1ffcd67562 changelog 2026-08-10 10:36:01 -07:00
Calum H. fda5c62bc6 fix: ignore extensions for mrpack warning modal (#6948) 2026-08-10 17:25:52 +00:00
aecsocket f8f05ce91b fix: expose raw icon URL in project response (#7086)
* fix: expose raw icon URL in project response

* remove unnecessary test
2026-08-10 15:48:23 +00:00
Calum H.andProspector 8b2438aebb fix: Gifs in the project description are not working properly (#7076)
* fix: Gifs in the project description are not working properly
Fixes #3786

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-08-10 15:43:13 +00:00
aecsocket 07bac8ffde fix: v3 environment fields in all v2 endpoints (#7082) 2026-08-10 15:42:56 +00:00
10 changed files with 105 additions and 11 deletions
+23
View File
@@ -28,6 +28,8 @@ pub struct LegacyProject {
pub server_side: LegacySideType,
/// A list of game versions this project supports
pub game_versions: Vec<String>,
/// The environments this project supports
pub environment: Vec<String>,
// All other fields are the same as V3
// If they change, or their constituent types change, we may need to
@@ -56,6 +58,7 @@ pub struct LegacyProject {
pub loaders: Vec<String>,
pub versions: Vec<VersionId>,
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub issues_url: Option<String>,
pub source_url: Option<String>,
pub wiki_url: Option<String>,
@@ -125,6 +128,14 @@ impl LegacyProject {
.filter_map(|v| v.as_str())
.map(|v| v.to_string())
.collect();
let environment = data
.fields
.get("environment")
.unwrap_or(&Vec::new())
.iter()
.filter_map(|v| v.as_str())
.map(|v| v.to_string())
.collect();
if let Some(versions_item) = versions_item {
// Extract side types from remaining fields
@@ -205,6 +216,7 @@ impl LegacyProject {
loaders,
versions: data.versions,
icon_url: data.icon_url,
raw_icon_url: data.raw_icon_url,
issues_url,
source_url,
wiki_url,
@@ -221,6 +233,7 @@ impl LegacyProject {
client_side,
server_side,
game_versions,
environment,
}
}
@@ -302,6 +315,9 @@ pub struct LegacyVersion {
/// A list of loaders this project supports (has a newtype struct)
pub loaders: Vec<Loader>,
/// The environment this version supports
pub environment: String,
pub id: VersionId,
pub project_id: ProjectId,
pub author_id: UserId,
@@ -332,6 +348,12 @@ impl From<Version> for LegacyVersion {
}
}
}
let environment = data
.fields
.get("environment")
.and_then(|value| value.as_str())
.unwrap_or("unknown")
.to_string();
// - if loader is mrpack, this is a modpack
// the v2 loaders are whatever the corresponding loader fields are
@@ -366,6 +388,7 @@ impl From<Version> for LegacyVersion {
dependencies: data.dependencies,
game_versions,
loaders,
environment,
}
}
}
+10
View File
@@ -41,6 +41,7 @@ pub struct LegacyResultSearchProject {
pub license: String,
pub client_side: String,
pub server_side: String,
pub environment: Vec<String>,
pub gallery: Vec<String>,
pub featured_gallery: Option<String>,
pub color: Option<u32>,
@@ -118,6 +119,14 @@ impl LegacyResultSearchProject {
let environment =
get_one_string_loader_field("environment").unwrap_or("unknown");
let environments = result_search_project
.loader_fields
.get("environment")
.cloned()
.unwrap_or_default()
.into_iter()
.filter_map(|environment| environment.as_str().map(String::from))
.collect();
let (client_side, server_side) =
v2_reroute::convert_v3_environment_to_v2_side_types(
@@ -141,6 +150,7 @@ impl LegacyResultSearchProject {
all_project_types: result_search_project.all_project_types,
client_side,
server_side,
environment: environments,
versions,
latest_version: result_search_project.version_id,
categories,
+3
View File
@@ -79,6 +79,8 @@ pub struct Project {
pub versions: Vec<VersionId>,
/// The URL of the icon of the project
pub icon_url: Option<String>,
/// The URL of the unoptimized icon of the project
pub raw_icon_url: Option<String>,
/// A collection of links to the project's various pages.
pub link_urls: HashMap<String, Link>,
@@ -193,6 +195,7 @@ impl From<ProjectQueryResult> for Project {
loaders: m.loaders,
versions: data.versions.into_iter().map(|v| v.into()).collect(),
icon_url: m.icon_url,
raw_icon_url: m.raw_icon_url,
link_urls: data
.urls
.into_iter()
@@ -1044,6 +1044,7 @@ async fn project_create_inner(
.map(|v| v.version_id.into())
.collect::<Vec<_>>(),
icon_url: project_builder.icon_url.clone(),
raw_icon_url: project_builder.raw_icon_url.clone(),
link_urls: project_builder
.link_urls
.clone()
@@ -55,6 +55,7 @@ pub struct CommonProject {
pub loaders: Vec<String>,
pub versions: Vec<VersionId>,
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub gallery: Vec<GalleryItem>,
pub color: Option<u32>,
pub thread_id: ThreadId,
+1
View File
@@ -378,6 +378,7 @@ async fn search_projects() {
for hit in client_side_optional_server_side_optional.hits {
assert_eq!(hit.client_side, "optional".to_string());
assert_eq!(hit.server_side, "optional".to_string());
assert_eq!(hit.environment, vec!["client_or_server".to_string()]);
}
// Ensure game_versions return correctly, but also correctly aggregated
+11 -1
View File
@@ -7,7 +7,7 @@ use futures::StreamExt;
use labrinth::{
models::ids::VersionId,
models::projects::{Loader, VersionStatus, VersionType},
models::v2::projects::LegacySideType,
models::v2::projects::{LegacySideType, LegacyVersion},
routes::v2::version_file::FileUpdateData,
};
use serde_json::json;
@@ -550,6 +550,8 @@ async fn add_version_accepts_environment_v2() {
)
.await;
assert_status!(&resp, StatusCode::OK);
let version: LegacyVersion = test::read_body_json(resp).await;
assert_eq!(version.environment, "server_only_client_optional");
let project = api
.get_project_deserialized(
@@ -559,6 +561,10 @@ async fn add_version_accepts_environment_v2() {
.await;
assert_eq!(project.client_side, LegacySideType::Optional);
assert_eq!(project.server_side, LegacySideType::Required);
assert_eq!(
project.environment,
vec!["server_only_client_optional".to_string()]
);
},
)
.await;
@@ -589,6 +595,10 @@ async fn create_project_initial_version_accepts_environment_v2() {
api.get_project_deserialized(slug, USER_USER_PAT).await;
assert_eq!(project.client_side, LegacySideType::Required);
assert_eq!(project.server_side, LegacySideType::Optional);
assert_eq!(
project.environment,
vec!["client_only_server_optional".to_string()]
);
},
)
.await;
@@ -1,4 +1,5 @@
use crate::State;
use crate::api::instance::CONFIG_FILE_EXTENSIONS;
use crate::event::emit::loading_try_for_each_concurrent;
use crate::install::{
InstallErrorContext, InstallJobEventKind, InstallPhaseDetails,
@@ -43,6 +44,19 @@ type ExtractProgressFn<'a> = dyn FnMut(u64) -> Pin<Box<dyn Future<Output = crate
+ 'a;
type HashProgressFn<'a> = dyn FnMut(u64) -> crate::Result<()> + Send + 'a;
const MODPACK_CONTENT_DOWNLOAD_CONCURRENCY: usize = 4;
const MRPACK_WARNING_IGNORED_EXTENSIONS: &[&str] = &["rpo"];
fn is_ignored_mrpack_warning_file(path: &str) -> bool {
Path::new(path)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
CONFIG_FILE_EXTENSIONS
.iter()
.chain(MRPACK_WARNING_IGNORED_EXTENSIONS)
.any(|candidate| extension.eq_ignore_ascii_case(candidate))
})
}
#[derive(Clone)]
struct ModpackContentInstallContext {
@@ -267,6 +281,9 @@ pub(crate) async fn get_external_files_from_mrpack(
.into_iter()
.filter_map(|file| {
let path = file.path.as_str();
if is_ignored_mrpack_warning_file(path) {
return None;
}
let hash = file.hashes.get(&PackFileHash::Sha1)?.clone();
let file_name = path.rsplit('/').next()?.to_string();
Some((file_name, hash))
@@ -371,6 +388,7 @@ fn external_override_relative_path(path: &str) -> Option<&str> {
.strip_prefix("overrides/")
.or_else(|| path.strip_prefix("client-overrides/"))?;
(!path.ends_with('/')
&& !is_ignored_mrpack_warning_file(relative_path)
&& ProjectType::get_from_parent_folder(relative_path).is_some())
.then_some(relative_path)
}
+33
View File
@@ -10,6 +10,39 @@ export type VersionEntry = {
}
const VERSIONS: VersionEntry[] = [
{
date: `2026-08-10T17:34:55+00:00`,
product: 'app',
version: '0.17.5',
body: `## Changed
- Common config file formats and RPO files are now hidden from the external modpack file warning modal.
- Updated text in unknown file warning.
- When viewing an instance's content in the app, content updates will now be checked immediately, rather than waiting for cache to be invalidated. It may take up to 10 minutes for updates to be shown in the content tab.
- Updated translations. Want to help translate Modrinth App? [Click here](https://translate.modrinth.com)
## Fixed
- Fixed issue with animated GIFs sometimes not working in project page descriptions.
- Fixed issue on Windows and Linux where the window close button was incorrectly coloured.
- Fixed an issue when clicking content in the content tab of an instance it show a "Instance not found" notification
- Fixed issue with the file information (name + file size) on the download button on project version pages not showing up.
- Fixed broken buttons on the "Minecraft account required" modal on Windows.
- Fixed the right sidebar closing when collapsing friend list sections when using the "Hide right sidebar" option.`,
},
{
date: `2026-08-10T17:34:55+00:00`,
product: 'web',
body: `## Changed
- Updated translations. Want to help translate the Modrinth website? [Click here](https://translate.modrinth.com)
## Fixed
- Fixed issue with animated GIFs sometimes not working in project page descriptions.`,
},
{
date: `2026-08-10T17:34:55+00:00`,
product: 'hosting',
body: `## Changed
- Updated translations. Want to help translate Modrinth Hosting? [Click here](https://translate.modrinth.com)`,
},
{
date: `2026-08-08T19:10:34+00:00`,
product: 'web',
+4 -10
View File
@@ -90,7 +90,7 @@ export const configuredXss = new FilterXSS({
!value.startsWith('data:')
) {
try {
const url = new URL(value)
const url = new URL(value.replaceAll('&amp;', '&'))
if (url.hostname.includes('wsrv.nl')) {
url.searchParams.delete('errorredirect')
@@ -124,16 +124,10 @@ export const configuredXss = new FilterXSS({
!allowedHostnames.includes(url.hostname) &&
!allowedHostnameSuffixes.some((suffix) => url.hostname.endsWith(suffix))
) {
return safeAttrValue(
tag,
name,
`https://wsrv.nl/?url=${encodeURIComponent(
url.toString().replaceAll('&amp;', '&'),
)}&n=-1`,
cssFilter,
)
const proxiedUrl = `https://wsrv.nl/?url=${encodeURIComponent(url.toString())}&n=-1`
return safeAttrValue(tag, name, proxiedUrl.replaceAll('&', '&amp;'), cssFilter)
}
return safeAttrValue(tag, name, url.toString(), cssFilter)
return safeAttrValue(tag, name, url.toString().replaceAll('&', '&amp;'), cssFilter)
} catch {
/* empty */
}