Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,12 @@ public class ReactImageView(
// no worth marking as dirty if it already rendered.
}

/** Sets the request headers. A change rebuilds the image request on the next update. */
public fun setHeaders(headers: ReadableMap?) {
this.headers = headers
if (this.headers != headers) {
this.headers = headers
isDirty = true
}
}

// Disable rasterizing to offscreen layer in order to preserve background effects like box-shadow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import android.graphics.Color
import android.util.DisplayMetrics
import com.facebook.common.logging.FLog
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder
import com.facebook.drawee.drawable.ScalingUtils
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.CatalystInstance
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
import com.facebook.react.common.ReactConstants
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.LengthPercentage
Expand All @@ -40,12 +43,15 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockedStatic
import org.mockito.Mockito.RETURNS_SELF
import org.mockito.Mockito.anyString
import org.mockito.Mockito.mockStatic
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.reset
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
Expand Down Expand Up @@ -281,4 +287,27 @@ class ReactImagePropertyTest {
viewManager.setHeaders(mockView, headers)
verify(mockView).setHeaders(headers)
}

@Test
fun testHeadersChangeRebuildsImageRequest() {
val controllerBuilder = mock<PipelineDraweeControllerBuilder>(defaultAnswer = RETURNS_SELF)
val viewManager = ReactImageManager(controllerBuilder)
val view = viewManager.createViewInstance(themeContext)
val sources = Arguments.createArray()
sources.pushMap(JavaOnlyMap.of("uri", "https://example.com/image.png"))
viewManager.setSource(view, sources)
viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-1"))
view.maybeUpdateView()

viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-2"))
view.maybeUpdateView()
viewManager.setHeaders(view, JavaOnlyMap.of("Authorization", "token-2"))
view.maybeUpdateView()

val requests = argumentCaptor<ImageRequest>()
verify(controllerBuilder, times(2)).setImageRequest(requests.capture())
assertThat(requests.firstValue.sourceUri).isEqualTo(requests.secondValue.sourceUri)
assertThat((requests.secondValue as ReactNetworkImageRequest).headers)
.isEqualTo(JavaOnlyMap.of("Authorization", "token-2"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include <react/renderer/debug/debugStringConvertibleUtils.h>
#include <react/renderer/graphics/Color.h>

#include <algorithm>
#include <tuple>

namespace facebook::react {

ImageProps::ImageProps(
Expand Down Expand Up @@ -145,6 +148,30 @@ static folly::dynamic convertEdgeInsets(const EdgeInsets& edgeInsets) {
return edgeInsetsResult;
}

// ImageSource::operator== compares only the request identity: type and URI.
static bool hasSamePayload(const ImageSource& lhs, const ImageSource& rhs) {
return std::tie(
lhs.type,
lhs.uri,
lhs.bundle,
lhs.scale,
lhs.size,
lhs.body,
lhs.method,
lhs.cache,
lhs.headers) ==
std::tie(
rhs.type,
rhs.uri,
rhs.bundle,
rhs.scale,
rhs.size,
rhs.body,
rhs.method,
rhs.cache,
rhs.headers);
}

ComponentName ImageProps::getDiffPropsImplementationTarget() const {
return "Image";
}
Expand All @@ -158,11 +185,14 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const {

folly::dynamic result = ViewProps::getDiffProps(oldProps);

if (sources != oldProps->sources) {
if (!std::ranges::equal(sources, oldProps->sources, hasSamePayload)) {
auto sourcesArray = folly::dynamic::array();
for (const auto& source : sources) {
sourcesArray.push_back(toDynamic(source));
}
if (!sources.empty()) {
result["headers"] = sourcesArray[0]["headers"];
}
result["source"] = sourcesArray;
}

Expand Down Expand Up @@ -221,7 +251,8 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const {
SharedColor prevTintColorValue =
oldProps->tintColor.value_or(SharedColor{});
if (tintColorValue != prevTintColorValue) {
result["tintColor"] = *tintColorValue;
result["tintColor"] = tintColorValue ? folly::dynamic(*tintColorValue)
: folly::dynamic(nullptr);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <gtest/gtest.h>

#include <react/renderer/components/image/ImageProps.h>
#include <react/test_utils/PropsPayloads.h>

#ifdef RN_SERIALIZABLE_STATE

namespace facebook::react {

namespace {

using folly::dynamic;

// Props 2.0 serializes every field of a source, JS only the ones it sets.
void expectPayloads(const std::vector<PropsCommit>& commits) {
expectPayloadsMatchRawProps<ImageProps>(commits, {"source"});
}

} // namespace

// ReactImageView.setSource reads width and height at the top level of each
// source when there are several, and ReactImageManager reads the request
// headers from the top-level headers prop, which JS derives from the first
// source.
TEST(ImagePropsDiffTest, multipleSourcesCarrySizesAndHeaders) {
dynamic headers = dynamic::object("Authorization", "token");
expectPayloads({{dynamic::object(
"source",
dynamic::array(
dynamic::object("uri", "https://a")("width", 10)("height", 20)(
"headers", headers),
dynamic::object("uri", "https://b")("width", 30)("height", 40)))(
"headers", headers)}});
}

// ReactImageManager ignores a null headers prop, so only an empty map clears
// the headers of the previous source.
TEST(ImagePropsDiffTest, clearsHeadersWhenTheNewSourceHasNone) {
dynamic headers = dynamic::object("Authorization", "old");
expectPayloads({
{dynamic::object(
"source",
dynamic::array(
dynamic::object("uri", "https://a")("headers", headers)))(
"headers", headers)},
{dynamic::object(
"source", dynamic::array(dynamic::object("uri", "https://b")))(
"headers", nullptr),
dynamic::object("headers", dynamic::object())},
});
}

// ReactImageManager clears the tint only for null; 0 is a transparent SRC_IN
// tint that hides the image.
TEST(ImagePropsDiffTest, removingTintColorEmitsNull) {
expectPayloads({
{dynamic::object("tintColor", static_cast<int32_t>(0xFFFF0000))},
{dynamic::object("tintColor", nullptr)},
});
}

// ReactImageView reads the cache policy and the sizes of each source, and
// ReactImageManager the headers; a source with the same URI can change them.
TEST(ImagePropsDiffTest, emitsASameUriSourceWhenItsOtherFieldsChange) {
dynamic headers = dynamic::object("Authorization", "old");
dynamic source = dynamic::object("uri", "https://a")("width", 10)(
"height", 20)("scale", 1)("cache", "default")("headers", headers);
std::vector<PropsCommit> commits{
{dynamic::object("source", dynamic::array(source))("headers", headers)}};
dynamic changes = dynamic::object("cache", "reload")("width", 30)("scale", 2);
for (const auto& [field, value] : changes.items()) {
source[field] = value;
// Props 2.0 sends the first source's headers whenever the sources change.
commits.push_back(
{dynamic::object("source", dynamic::array(source)),
dynamic::object("headers", headers)});
}
source["headers"] = dynamic::object("Authorization", "new");
commits.push_back({dynamic::object("source", dynamic::array(source))(
"headers", source["headers"])});
expectPayloads(commits);
}

} // namespace facebook::react

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ static void updateBorderWidthPropValue(
if (newValue.has_value()) {
result[propName] = newValue.value();
} else {
result[propName] = NULL;
result[propName] = nullptr;
}
}
}
Expand Down Expand Up @@ -449,6 +449,56 @@ inline static void updateAccessibilityStateProp(
result["accessibilityState"] = resultState;
}

static folly::dynamic toDynamic(
const std::variant<std::monostate, ValueUnit>& lengthPercentageOrAuto) {
const auto* valueUnit = std::get_if<ValueUnit>(&lengthPercentageOrAuto);
return valueUnit != nullptr ? valueUnit->toDynamic() : "auto";
}

static folly::dynamic toDynamic(const BackgroundSize& backgroundSize) {
if (const auto* keyword =
std::get_if<BackgroundSizeKeyword>(&backgroundSize)) {
return *keyword == BackgroundSizeKeyword::Cover ? "cover" : "contain";
}
const auto& size = std::get<BackgroundSizeLengthPercentage>(backgroundSize);
return folly::dynamic::object("x", toDynamic(size.x))("y", toDynamic(size.y));
}

// JS sends one edge per axis, but the parser keeps the default top and left
// next to a bottom or right.
static folly::dynamic toDynamic(const BackgroundPosition& backgroundPosition) {
folly::dynamic result = folly::dynamic::object();
if (backgroundPosition.bottom.has_value()) {
result["bottom"] = backgroundPosition.bottom->toDynamic();
} else if (backgroundPosition.top.has_value()) {
result["top"] = backgroundPosition.top->toDynamic();
}
if (backgroundPosition.right.has_value()) {
result["right"] = backgroundPosition.right->toDynamic();
} else if (backgroundPosition.left.has_value()) {
result["left"] = backgroundPosition.left->toDynamic();
}
return result;
}

static const char* toString(BackgroundRepeatStyle backgroundRepeatStyle) {
switch (backgroundRepeatStyle) {
case BackgroundRepeatStyle::Repeat:
return "repeat";
case BackgroundRepeatStyle::Space:
return "space";
case BackgroundRepeatStyle::Round:
return "round";
case BackgroundRepeatStyle::NoRepeat:
return "no-repeat";
}
}

static folly::dynamic toDynamic(const BackgroundRepeat& backgroundRepeat) {
return folly::dynamic::object("x", toString(backgroundRepeat.x))(
"y", toString(backgroundRepeat.y));
}

ComponentName HostPlatformViewProps::getDiffPropsImplementationTarget() const {
return "View";
}
Expand Down Expand Up @@ -528,7 +578,8 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
}

if (shadowColor != oldProps->shadowColor) {
result["shadowColor"] = *shadowColor;
result["shadowColor"] =
shadowColor ? folly::dynamic(*shadowColor) : folly::dynamic(nullptr);
}

if (shadowOpacity != oldProps->shadowOpacity) {
Expand Down Expand Up @@ -580,6 +631,18 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
result["backgroundImage"] = toDynamic(backgroundImage);
}

if (backgroundSize != oldProps->backgroundSize) {
result["backgroundSize"] = toDynamic(backgroundSize);
}

if (backgroundPosition != oldProps->backgroundPosition) {
result["backgroundPosition"] = toDynamic(backgroundPosition);
}

if (backgroundRepeat != oldProps->backgroundRepeat) {
result["backgroundRepeat"] = toDynamic(backgroundRepeat);
}

if (mixBlendMode != oldProps->mixBlendMode) {
result["mixBlendMode"] = toString(mixBlendMode);
}
Expand Down
Loading
Loading